diff options
author | Eric Bailey <git@esb.lol> | 2024-12-05 18:59:26 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-05 18:59:26 -0600 |
commit | 143e2c802d1d8d8498e6658c174ed1e657c4ec12 (patch) | |
tree | cbe937bec7e0a241774060ade7428180c4fe0aaf | |
parent | 8467dfd452b4cb1b62214b3abe87fd90d23a183b (diff) | |
download | voidsky-143e2c802d1d8d8498e6658c174ed1e657c4ec12.tar.zst |
[Layout] Base (#6907)
* Add common gutter styles as hook * Add computed scrollbar gutter CSS vars * Add new layout components * Replace layout components in settings screens * Remove old back button * Invert web border logic for easier migration * Clean up Slot API * Port over FF handling of scrollbar offset * Trade boilerplate for ease of use * Limit to one line * Allow two lines, fix wrapping * Fix alignment * sticky headers * set max with on header and center * [Layout] Notifications Header (#6910) * Replace notifications screen header * fix cropped indicator --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com> * Replace Hashtag header (#6928) * [Layout] ChatList header (#6929) * Replace ChatList header * update chat settings as well --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com> * Add web borders to Chat settings * Remove unused var * Move ChatList header outside center * Replace empty chat layout * fix breakpoints * [Layout] Scrollbar gutters (#6908) * Fix sidebar alignment * Make sure scrollbars don't hide * Gift left nav more space * Use stable one-edge, update logic in RightNav * Ope * Increase width * Reset * Add transform to sidebars * Remove bg in sidebars * Handle shifts in layout components * Replace scroll-removal handling * Make react-remove-scroll an explicit dep * Remove unused script * use correct scroll insets (#6950) * [Layout] Feeds headers (#6913) * Replace ViewHeader internals, duplicate old ViewHeader * Replace Feeds header * Replace SavedFeeds header * Visual alignment * Uglier but clear * Use old ViewHeader for SavedFeeds * use Layout.Center instead of Layout.Content * use left-aligned header for feed edit * delete unused old view header --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com> * [Layout] Every other screen (#6953) * attempt to fix double borders on every other screen * delete ListHeaderDesktop * delete `SimpleViewHeader` and fix screens (#6956) * Make Layout.Center not full height * Refactor List to use Layout.Center, remove built-in borders * Fix Home screen * Refactor PagerWithHeader to use Layout components * Replace components in ProfileFeed and ProfileList * Borders on Profile * Search screen replacements * use new header for profile subpage header (#6958) * Search AutocompleteResults * use new header for starter pack wizard (#6957) * Fix post thread * Enable borders by default * Moderation muted and blocked accounts * Fix scrollbar offset on Labeler * Remove ScrollView from Moderation * Remove ScrollView from Deactivated * Remove ScrollView from onboarding * Remove ScrollView from SignupQueued * Mark deprecations * fix lint * Fix double borders on profile load * Remove unneeded CenteredView from noty Feed * Remove double Center layout on Notifications screen * Remove double Center layout on ChatList screen * Handle scrollbar offset in chat * Use new atom for other scrollbar offsets * Remove borders from old views * Better doc * Remove temp migration prop * Fix new atom usage on native * Clean up Hashtag screen * Layout docs * Clarify usage in Pager * Handle nested offset contexts * Clean up Layout * fix feeds page * asymmetric header on native (#6969) * Reusable header const * Fix up home header * Add back button to convo * Add hitslop to header buttons * Comment * Better handling on native for new atom * Format * Fix nested flatlist on mod screens * Use react-remove-scroll-bar directly * Fix notification count overflow on web * Clarify doc --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com>
89 files changed, 1721 insertions, 2048 deletions
diff --git a/assets/icons/floppyDisk_stroke2_corner0_rounded.svg b/assets/icons/floppyDisk_stroke2_corner0_rounded.svg new file mode 100644 index 000000000..b9a42f759 --- /dev/null +++ b/assets/icons/floppyDisk_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h13a1 1 0 0 1 .707.293l3 3A1 1 0 0 1 21 7v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm6 15h6v-5H9v5Zm8 0v-6a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v6H5V5h2v3a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5.414l2 2V19h-2ZM15 5H9v2h6V5Z" clip-rule="evenodd"/></svg> diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index 4c02805e3..8eb78fffd 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -40,7 +40,6 @@ } html { background-color: white; - scrollbar-gutter: stable both-edges; } @media (prefers-color-scheme: dark) { html { @@ -76,9 +75,15 @@ top: 50%; transform: translateX(-50%) translateY(-50%) translateY(-50px); } - /* We need this style to prevent web dropdowns from shifting the display when opening */ + /** + * We need these styles to prevent shifting due to scrollbar show/hide on + * OSs that have them enabled by default. This also handles cases where the + * screen wouldn't otherwise scroll, and therefore hide the scrollbar and + * shift the content, by forcing the page to show a scrollbar. + */ body { width: 100%; + overflow-y: scroll; } </style> diff --git a/package.json b/package.json index 6372dd5f5..6c35d169e 100644 --- a/package.json +++ b/package.json @@ -193,6 +193,7 @@ "react-native-web": "~0.19.11", "react-native-web-webview": "^1.0.2", "react-native-webview": "13.10.2", + "react-remove-scroll-bar": "^2.3.6", "react-responsive": "^9.0.2", "react-textarea-autosize": "^8.5.3", "rn-fetch-blob": "^0.12.0", diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index 1f08eb7e1..0870c5767 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -1,7 +1,8 @@ import {Platform, StyleProp, StyleSheet, ViewStyle} from 'react-native' import * as tokens from '#/alf/tokens' -import {ios, native, web} from '#/alf/util/platform' +import {ios, native, platform, web} from '#/alf/util/platform' +import * as Layout from '#/components/Layout' export const atoms = { debug: { @@ -21,6 +22,9 @@ export const atoms = { relative: { position: 'relative', }, + sticky: web({ + position: 'sticky', + }), inset_0: { top: 0, left: 0, @@ -941,4 +945,20 @@ export const atoms = { transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)', transitionDuration: '100ms', }), + + /** + * {@link Layout.SCROLLBAR_OFFSET} + */ + scrollbar_offset: platform({ + web: { + transform: [ + { + translateX: Layout.SCROLLBAR_OFFSET, + }, + ], + }, + native: { + transform: [], + }, + }) as {transform: Exclude<ViewStyle['transform'], string | undefined>}, } as const diff --git a/src/alf/index.tsx b/src/alf/index.tsx index 5d08722ff..a96803c56 100644 --- a/src/alf/index.tsx +++ b/src/alf/index.tsx @@ -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 diff --git a/src/alf/util/useGutterStyles.ts b/src/alf/util/useGutterStyles.ts new file mode 100644 index 000000000..64b246fdd --- /dev/null +++ b/src/alf/util/useGutterStyles.ts @@ -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]) +} diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index 6b92eee3e..e45133dc5 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -12,6 +12,7 @@ import {useLingui} from '@lingui/react' import {DismissableLayer} from '@radix-ui/react-dismissable-layer' import {useFocusGuards} from '@radix-ui/react-focus-guards' import {FocusScope} from '@radix-ui/react-focus-scope' +import {RemoveScrollBar} from 'react-remove-scroll-bar' import {logger} from '#/logger' import {useDialogStateControlContext} from '#/state/dialogs' @@ -103,6 +104,7 @@ export function Outer({ {isOpen && ( <Portal> <Context.Provider value={context}> + <RemoveScrollBar /> <TouchableWithoutFeedback accessibilityHint={undefined} accessibilityLabel={_(msg`Close active dialog`)} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx deleted file mode 100644 index ea11e2217..000000000 --- a/src/components/Layout.tsx +++ /dev/null @@ -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} diff --git a/src/components/Layout/Header/index.tsx b/src/components/Layout/Header/index.tsx new file mode 100644 index 000000000..a35a09537 --- /dev/null +++ b/src/components/Layout/Header/index.tsx @@ -0,0 +1,199 @@ +import {createContext, useCallback, useContext} from 'react' +import {GestureResponderEvent, View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' + +import {HITSLOP_30} from '#/lib/constants' +import {NavigationProp} from '#/lib/routes/types' +import {isIOS} from '#/platform/detection' +import {useSetDrawerOpen} from '#/state/shell' +import { + atoms as a, + platform, + TextStyleProp, + useBreakpoints, + useGutterStyles, + useTheme, +} from '#/alf' +import {Button, ButtonIcon, ButtonProps} 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 { + BUTTON_VISUAL_ALIGNMENT_OFFSET, + HEADER_SLOT_SIZE, +} from '#/components/Layout/const' +import {ScrollbarOffsetContext} from '#/components/Layout/context' +import {Text} from '#/components/Typography' + +export function Outer({ + children, + noBottomBorder, +}: { + children: React.ReactNode + noBottomBorder?: boolean +}) { + const t = useTheme() + const gutter = useGutterStyles() + const {gtMobile} = useBreakpoints() + const {isWithinOffsetView} = useContext(ScrollbarOffsetContext) + + return ( + <View + style={[ + a.w_full, + !noBottomBorder && a.border_b, + a.flex_row, + a.align_center, + a.gap_sm, + gutter, + platform({ + native: [a.pb_sm, a.pt_xs], + web: [a.py_sm], + }), + t.atoms.border_contrast_low, + gtMobile && [a.mx_auto, {maxWidth: 600}], + !isWithinOffsetView && a.scrollbar_offset, + ]}> + {children} + </View> + ) +} + +const AlignmentContext = createContext<'platform' | 'left'>('platform') + +export function Content({ + children, + align = 'platform', +}: { + children?: React.ReactNode + align?: 'platform' | 'left' +}) { + return ( + <View + style={[ + a.flex_1, + a.justify_center, + isIOS && align === 'platform' && a.align_center, + {minHeight: HEADER_SLOT_SIZE}, + ]}> + <AlignmentContext.Provider value={align}> + {children} + </AlignmentContext.Provider> + </View> + ) +} + +export function Slot({children}: {children?: React.ReactNode}) { + return ( + <View + style={[ + a.z_50, + { + width: HEADER_SLOT_SIZE, + }, + ]}> + {children} + </View> + ) +} + +export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) { + const {_} = useLingui() + const navigation = useNavigation<NavigationProp>() + + const onPressBack = useCallback( + (evt: GestureResponderEvent) => { + onPress?.(evt) + if (evt.defaultPrevented) return + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.navigate('Home') + } + }, + [onPress, navigation], + ) + + return ( + <Slot> + <Button + label={_(msg`Go back`)} + size="small" + variant="ghost" + color="secondary" + shape="square" + onPress={onPressBack} + hitSlop={HITSLOP_30} + style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}, style]} + {...props}> + <ButtonIcon icon={ArrowLeft} size="lg" /> + </Button> + </Slot> + ) +} + +export function MenuButton() { + const {_} = useLingui() + const setDrawerOpen = useSetDrawerOpen() + const {gtMobile} = useBreakpoints() + + const onPress = useCallback(() => { + setDrawerOpen(true) + }, [setDrawerOpen]) + + return gtMobile ? null : ( + <Slot> + <Button + label={_(msg`Open drawer menu`)} + size="small" + variant="ghost" + color="secondary" + shape="square" + onPress={onPress} + hitSlop={HITSLOP_30} + style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}]}> + <ButtonIcon icon={Menu} size="lg" /> + </Button> + </Slot> + ) +} + +export function TitleText({ + children, + style, +}: {children: React.ReactNode} & TextStyleProp) { + const {gtMobile} = useBreakpoints() + const align = useContext(AlignmentContext) + return ( + <Text + style={[ + a.text_lg, + a.font_heavy, + a.leading_tight, + isIOS && align === 'platform' && a.text_center, + gtMobile && a.text_xl, + style, + ]} + numberOfLines={2}> + {children} + </Text> + ) +} + +export function SubtitleText({children}: {children: React.ReactNode}) { + const t = useTheme() + const align = useContext(AlignmentContext) + return ( + <Text + style={[ + a.text_sm, + a.leading_snug, + isIOS && align === 'platform' && a.text_center, + t.atoms.text_contrast_medium, + ]} + numberOfLines={2}> + {children} + </Text> + ) +} diff --git a/src/components/Layout/README.md b/src/components/Layout/README.md new file mode 100644 index 000000000..1bcc3489e --- /dev/null +++ b/src/components/Layout/README.md @@ -0,0 +1,172 @@ +# Layout + +This directory contains our core layout components. Use these when creating new +screens, or when supplementing other components with functionality like +centering. + +## Usage + +If we aren't talking about the `shell` components, layouts on individual screens +look like more or less like this: + +```tsx +<Outer> + <Header>...</Header> + <Content>...</Content> +</Outer> +``` + +I'll map these words to real components. + +### `Layout.Screen` + +Provides the "Outer" functionality for a screen, like taking up the full height +of the screen. **All screens should be wrapped with this component,** probably +as the outermost component. + +> [!NOTE] +> On web, `Layout.Screen` also provides the side borders on our central content +> column. These borders are fixed position, 1px outside our center column width +> of 600px. +> +> What this effectively means is that _nothing inside the center content column +> needs (or should) define left/right borders._ That is now handled in one +> place: within `Layout.Screen`. + +### `Layout.Header.*` + +The `Layout.Header` component actually contains multiple sub-components. Use +this to compose different versions of the header. The most basic version looks +like this: + +```tsx +<Layout.Header.Outer> + <Layout.Header.BackButton /> {/* or <Layout.Header.MenuButton /> */} + + <Layout.Header.Content> + <Layout.Header.TitleText>Account</Layout.Header.TitleText> + + {/* Optional subtitle */} + <Layout.Header.SubtitleText>Settings for @esb.lol</Layout.Header.SubtitleText> + </Layout.Header.Content> + + <Layout.Header.Slot /> +</Layout.Header.Outer> +``` + +Note the additional `Slot` component. This is here to keep the header balanced +and provide correct spacing on all platforms. The `Slot` is 34px wide, which +matches the `BackButton` and `MenuButton`. + +> If anyone has better ideas, I'm all ears, but this was simple and the small +> amount of boilerplate is only incurred when creating a new screen, which is +> infrequent. + +It can also function as a "slot" for a button positioned on the right side. See +the `Hashtag` screen for an example, abbreviated below: + +```tsx +<Layout.Header.Slot> + <Button size='small' shape='round'>...</Button> +</Layout.Header.Slot> +``` + +If you need additional customization, simply use the components that are helpful +and create new ones as needed. A good example is the `SavedFeeds` screen, which +looks roughly like this: + +```tsx +<Layout.Header.Outer> + <Layout.Header.BackButton /> + + {/* Override to align content to the left, making room for the button */} + <Layout.Header.Content align='left'> + <Layout.Header.TitleText>Edit My Feeds</Layout.Header.TitleText> + </Layout.Header.Content> + + {/* Custom button, wider than 34px */} + <Button size='small'>...</Button> +</Layout.Header.Outer> +``` + +> [!TIP] +> The `Header` should be _outside_ the `Content` component in order to be +> fixed on scroll on native. Placing it inside will make it scroll with the rest +> of the page. + +### `Layout.Content` + +This provides the "Content" functionality for a screen. This component is +actually an `Animated.ScrollView`, and accepts props for that component. It +provides a little default styling as well. On web, it also _centers the content +inside our center content column of 600px_. + +> [!NOTE] +> What about flatlists or pagers? Those components are not colocated here (yet). +> But those components serve the same purpose of "Content". + +## Examples + +The most basic layout available to us looks like this: + +```tsx +<Layout.Screen> + <Layout.Header.Outer> + <Layout.Header.BackButton /> {/* or <Layout.Header.MenuButton /> */} + + <Layout.Header.Content> + <Layout.Header.TitleText>Account</Layout.Header.TitleText> + + {/* Optional subtitle */} + <Layout.Header.SubtitleText>Settings for @esb.lol</Layout.Header.SubtitleText> + </Layout.Header.Content> + + <Layout.Header.Slot /> + </Layout.Header.Outer> + + <Layout.Content> + ... + </Layout.Content> +</Layout.Screen> +``` + +**For `List` views,** you'd sub in `List` for `Layout.Content` and it will +function the same. See `Feeds` screen for an example. + +**For `Pager` views,** including `PagerWithHeader`, do the same. See `Hashtag` +screen for an example. + +## Utilities + +### `Layout.Center` + +This component behaves like our old `CenteredView` component. + +### `Layout.SCROLLBAR_OFFSET` and `Layout.SCROLLBAR_OFFSET_POSITIVE` + +Provide a pre-configured CSS vars for use when aligning fixed position elements. +More on this below. + +## Scrollbar gutter handling + +Operating systems allow users to configure if their browser _always_ shows +scrollbars not. Some OSs also don't allow configuration. + +The presence of scrollbars affects layout, particularly fixed position elements. +Browsers support `scrollbar-gutter`, but each behaves differently. Our approach +is to use the default `scrollbar-gutter: auto`. Basically, we start from a clean +slate. + +This handling becomes particularly thorny when we need to lock scroll, like when +opening a dialog or dropdown. Radix uses the library `react-remove-scroll` +internally, which in turn depends on +[`react-remove-scroll-bar`](https://github.com/theKashey/react-remove-scroll-bar). +We've opted to rely on this transient dependency. This library adds some utility +classes and CSS vars to the page when scroll is locked. + +**It is this CSS variable that we use in `SCROLLBAR_OFFSET` values.** This +ensures that elements do not shift relative to the screen when opening a +dropdown or dialog. + +These styles are applied where needed and we should have very little need of +adjusting them often. diff --git a/src/components/Layout/const.ts b/src/components/Layout/const.ts new file mode 100644 index 000000000..11825d323 --- /dev/null +++ b/src/components/Layout/const.ts @@ -0,0 +1,16 @@ +export const SCROLLBAR_OFFSET = + 'calc(-1 * var(--removed-body-scroll-bar-size, 0px) / 2)' as any +export const SCROLLBAR_OFFSET_POSITIVE = + 'calc(var(--removed-body-scroll-bar-size, 0px) / 2)' as any + +/** + * Useful for visually aligning icons within header buttons with the elements + * below them on the screen. Apply positively or negatively depending on side + * of the screen you're on. + */ +export const BUTTON_VISUAL_ALIGNMENT_OFFSET = 3 + +/** + * Corresponds to the width of a small square or round button + */ +export const HEADER_SLOT_SIZE = 34 diff --git a/src/components/Layout/context.ts b/src/components/Layout/context.ts new file mode 100644 index 000000000..8e0c5445e --- /dev/null +++ b/src/components/Layout/context.ts @@ -0,0 +1,5 @@ +import React from 'react' + +export const ScrollbarOffsetContext = React.createContext({ + isWithinOffsetView: false, +}) diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx new file mode 100644 index 000000000..d08505fbf --- /dev/null +++ b/src/components/Layout/index.tsx @@ -0,0 +1,188 @@ +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, + useAnimatedProps, +} from 'react-native-reanimated' +import {useSafeAreaInsets} from 'react-native-safe-area-context' + +import {isWeb} from '#/platform/detection' +import {useShellLayout} from '#/state/shell/shell-layout' +import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {ScrollbarOffsetContext} from '#/components/Layout/context' + +export * from '#/components/Layout/const' +export * as Header from '#/components/Layout/Header' + +export type ScreenProps = React.ComponentProps<typeof View> & { + style?: StyleProp<ViewStyle> +} + +/** + * Outermost component of every screen + */ +export const Screen = React.memo(function Screen({ + style, + ...props +}: ScreenProps) { + const {top} = useSafeAreaInsets() + return ( + <> + {isWeb && <WebCenterBorders />} + <View + style={[a.util_screen_outer, {paddingTop: top}, style]} + {...props} + /> + </> + ) +}) + +export type ContentProps = AnimatedScrollViewProps & { + style?: StyleProp<ViewStyle> + contentContainerStyle?: StyleProp<ViewStyle> +} + +/** + * Default scroll view for simple pages + */ +export const Content = React.memo(function Content({ + children, + style, + contentContainerStyle, + ...props +}: ContentProps) { + const {footerHeight} = useShellLayout() + const animatedProps = useAnimatedProps(() => { + return { + scrollIndicatorInsets: { + bottom: footerHeight.get(), + top: 0, + right: 1, + }, + } satisfies AnimatedScrollViewProps + }) + + return ( + <Animated.ScrollView + id="content" + automaticallyAdjustsScrollIndicatorInsets={false} + // sets the scroll inset to the height of the footer + animatedProps={animatedProps} + style={[scrollViewStyles.common, style]} + contentContainerStyle={[ + scrollViewStyles.contentContainer, + contentContainerStyle, + ]} + {...props}> + {isWeb ? ( + // @ts-ignore web only -esb + <Center>{children}</Center> + ) : ( + children + )} + </Animated.ScrollView> + ) +}) + +const scrollViewStyles = StyleSheet.create({ + common: { + width: '100%', + }, + contentContainer: { + paddingBottom: 100, + }, +}) + +export type KeyboardAwareContentProps = KeyboardAwareScrollViewProps & { + children: React.ReactNode + contentContainerStyle?: StyleProp<ViewStyle> +} + +/** + * Default scroll view for simple pages. + * + * BE SURE TO TEST THIS WHEN USING, it's untested as of writing this comment. + */ +export const KeyboardAwareContent = React.memo(function LayoutScrollView({ + children, + style, + contentContainerStyle, + ...props +}: KeyboardAwareContentProps) { + return ( + <KeyboardAwareScrollView + style={[scrollViewStyles.common, style]} + contentContainerStyle={[ + scrollViewStyles.contentContainer, + contentContainerStyle, + ]} + keyboardShouldPersistTaps="handled" + {...props}> + {isWeb ? <Center>{children}</Center> : children} + </KeyboardAwareScrollView> + ) +}) + +/** + * Utility component to center content within the screen + */ +export const Center = React.memo(function LayoutContent({ + children, + style, + ...props +}: ViewProps) { + const {isWithinOffsetView} = useContext(ScrollbarOffsetContext) + const {gtMobile} = useBreakpoints() + const ctx = useMemo(() => ({isWithinOffsetView: true}), []) + return ( + <View + style={[ + a.w_full, + a.mx_auto, + gtMobile && { + maxWidth: 600, + }, + style, + !isWithinOffsetView && a.scrollbar_offset, + ]} + {...props}> + <ScrollbarOffsetContext.Provider value={ctx}> + {children} + </ScrollbarOffsetContext.Provider> + </View> + ) +}) + +/** + * Only used within `Layout.Screen`, not for reuse + */ +const WebCenterBorders = React.memo(function LayoutContent() { + 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, + web({ + width: 602, + left: '50%', + transform: [ + { + translateX: '-50%', + }, + ...a.scrollbar_offset.transform, + ], + }), + ]} + /> + ) : null +}) diff --git a/src/components/LikedByList.tsx b/src/components/LikedByList.tsx index a83f98258..b369bd76e 100644 --- a/src/components/LikedByList.tsx +++ b/src/components/LikedByList.tsx @@ -12,8 +12,14 @@ import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' import {List} from '#/view/com/util/List' import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' -function renderItem({item}: {item: GetLikes.Like}) { - return <ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} /> +function renderItem({item, index}: {item: GetLikes.Like; index: number}) { + return ( + <ProfileCardWithFollowBtn + key={item.actor.did} + profile={item.actor} + noBorder={index === 0} + /> + ) } function keyExtractor(item: GetLikes.Like) { @@ -81,6 +87,8 @@ export function LikedByList({uri}: {uri: string}) { )} errorMessage={cleanError(resolveError || error)} onRetry={isError ? refetch : undefined} + topBorder={false} + sideBorders={false} /> ) } @@ -103,6 +111,7 @@ export function LikedByList({uri}: {uri: string}) { onEndReachedThreshold={3} initialNumToRender={initialNumToRender} windowSize={11} + sideBorders={false} /> ) } diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx index 16bd6a9ea..2d7b13b25 100644 --- a/src/components/Lists.tsx +++ b/src/components/Lists.tsx @@ -109,38 +109,6 @@ function ListFooterMaybeError({ ) } -export function ListHeaderDesktop({ - title, - subtitle, -}: { - title: string - subtitle?: string -}) { - const {gtTablet} = useBreakpoints() - const t = useTheme() - - if (!gtTablet) return null - - return ( - <View - style={[ - a.w_full, - a.py_sm, - a.px_xl, - a.gap_xs, - a.justify_center, - {minHeight: 50}, - ]}> - <Text style={[a.text_2xl, a.font_bold]}>{title}</Text> - {subtitle ? ( - <Text style={[a.text_md, t.atoms.text_contrast_medium]}> - {subtitle} - </Text> - ) : undefined} - </View> - ) -} - let ListMaybePlaceholder = ({ isLoading, noEmpty, @@ -154,7 +122,7 @@ let ListMaybePlaceholder = ({ onGoBack, hideBackButton, sideBorders, - topBorder = true, + topBorder = false, }: { isLoading: boolean noEmpty?: boolean diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index 6c3bbf216..acffa0c2b 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -65,25 +65,23 @@ export let MessagesListHeader = ({ a.pr_lg, a.py_sm, ]}> - {!gtTablet && ( - <TouchableOpacity - testID="conversationHeaderBackBtn" - onPress={onPressBack} - hitSlop={BACK_HITSLOP} - style={{width: 30, height: 30, marginTop: isWeb ? 6 : 4}} - accessibilityRole="button" - accessibilityLabel={_(msg`Back`)} - accessibilityHint=""> - <FontAwesomeIcon - size={18} - icon="angle-left" - style={{ - marginTop: 6, - }} - color={t.atoms.text.color} - /> - </TouchableOpacity> - )} + <TouchableOpacity + testID="conversationHeaderBackBtn" + onPress={onPressBack} + hitSlop={BACK_HITSLOP} + style={{width: 30, height: 30, marginTop: isWeb ? 6 : 4}} + accessibilityRole="button" + accessibilityLabel={_(msg`Back`)} + accessibilityHint=""> + <FontAwesomeIcon + size={18} + icon="angle-left" + style={{ + marginTop: 6, + }} + color={t.atoms.text.color} + /> + </TouchableOpacity> {profile && moderation && blockInfo ? ( <HeaderReady diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx index b64c0e9fa..58f4d4f89 100644 --- a/src/components/forms/DateField/index.android.tsx +++ b/src/components/forms/DateField/index.android.tsx @@ -57,6 +57,7 @@ export function DateField({ open timeZoneOffsetInMinutes={0} theme={t.scheme} + // @ts-ignore TODO buttonColor={t.name === 'light' ? '#000000' : '#ffffff'} date={new Date(value)} onConfirm={onChangeInternal} diff --git a/src/components/icons/FloppyDisk.tsx b/src/components/icons/FloppyDisk.tsx new file mode 100644 index 000000000..7fb938089 --- /dev/null +++ b/src/components/icons/FloppyDisk.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const FloppyDisk_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 4a1 1 0 0 1 1-1h13a1 1 0 0 1 .707.293l3 3A1 1 0 0 1 21 7v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm6 15h6v-5H9v5Zm8 0v-6a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v6H5V5h2v3a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5.414l2 2V19h-2ZM15 5H9v2h6V5Z', +}) diff --git a/src/lib/hooks/useWebBodyScrollLock.ts b/src/lib/hooks/useWebBodyScrollLock.ts deleted file mode 100644 index c63c23b29..000000000 --- a/src/lib/hooks/useWebBodyScrollLock.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {useEffect} from 'react' - -import {isWeb} from '#/platform/detection' - -let refCount = 0 - -function incrementRefCount() { - if (refCount === 0) { - document.body.style.overflow = 'hidden' - document.documentElement.style.scrollbarGutter = 'auto' - } - refCount++ -} - -function decrementRefCount() { - refCount-- - if (refCount === 0) { - document.body.style.overflow = '' - document.documentElement.style.scrollbarGutter = '' - } -} - -export function useWebBodyScrollLock(isLockActive: boolean) { - useEffect(() => { - if (!isWeb || !isLockActive) { - return - } - incrementRefCount() - return () => decrementRefCount() - }) -} diff --git a/src/screens/Deactivated.tsx b/src/screens/Deactivated.tsx index 36b96cacd..4fcb42854 100644 --- a/src/screens/Deactivated.tsx +++ b/src/screens/Deactivated.tsx @@ -17,13 +17,13 @@ import { } from '#/state/session' import {useSetMinimalShellMode} from '#/state/shell' import {useLoggedOutViewControls} from '#/state/shell/logged-out' -import {ScrollView} from '#/view/com/util/Views' import {Logo} from '#/view/icons/Logo' import {atoms as a, useTheme} from '#/alf' import {AccountList} from '#/components/AccountList' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Divider} from '#/components/Divider' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' +import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' @@ -104,24 +104,17 @@ export function Deactivated() { }, [_, agent, setPending, setError, queryClient]) return ( - <View style={[a.util_screen_outer, a.flex_1, t.atoms.bg]}> - <ScrollView - style={[ - a.h_full, - a.w_full, + <View style={[a.util_screen_outer, a.flex_1]}> + <Layout.Content + contentContainerStyle={[ a.px_2xl, { paddingTop: isWeb ? 64 : insets.top + 16, paddingBottom: isWeb ? 64 : insets.bottom, }, - ]} - contentContainerStyle={[ - a.w_full, - a.flex_row, - a.justify_center, - {borderWidth: 0}, ]}> - <View style={[a.w_full, {maxWidth: COL_WIDTH}]}> + <View + style={[a.w_full, {marginHorizontal: 'auto', maxWidth: COL_WIDTH}]}> <View style={[a.w_full, a.justify_center, a.align_center, a.pb_5xl]}> <Logo width={40} /> </View> @@ -218,7 +211,7 @@ export function Deactivated() { </> )} </View> - </ScrollView> + </Layout.Content> </View> ) } diff --git a/src/screens/Hashtag.tsx b/src/screens/Hashtag.tsx index adf5f0080..a0fc3707c 100644 --- a/src/screens/Hashtag.tsx +++ b/src/screens/Hashtag.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {ListRenderItemInfo, Pressable, View} from 'react-native' +import {ListRenderItemInfo, View} from 'react-native' import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -13,16 +13,15 @@ import {shareUrl} from '#/lib/sharing' import {cleanError} from '#/lib/strings/errors' import {sanitizeHandle} from '#/lib/strings/handles' import {enforceLen} from '#/lib/strings/helpers' -import {isNative, isWeb} from '#/platform/detection' import {useSearchPostsQuery} from '#/state/queries/search-posts' import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell' import {Pager} from '#/view/com/pager/Pager' import {TabBar} from '#/view/com/pager/TabBar' import {Post} from '#/view/com/post/Post' import {List} from '#/view/com/util/List' -import {ViewHeader} from '#/view/com/util/ViewHeader' -import {CenteredView} from '#/view/com/util/Views' -import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox' +import {atoms as a, web} from '#/alf' +import {Button, ButtonIcon} from '#/components/Button' +import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import * as Layout from '#/components/Layout' import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' @@ -110,46 +109,36 @@ export default function HashtagScreen({ return ( <Layout.Screen> - <CenteredView sideBorders={true}> - <ViewHeader - showOnDesktop - title={headerTitle} - subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined} - canGoBack - renderButton={ - isNative - ? () => ( - <Pressable - accessibilityRole="button" - onPress={onShare} - hitSlop={HITSLOP_10}> - <ArrowOutOfBox_Stroke2_Corner0_Rounded - size="lg" - onPress={onShare} - /> - </Pressable> - ) - : undefined - } - /> - </CenteredView> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText>{headerTitle}</Layout.Header.TitleText> + {author && ( + <Layout.Header.SubtitleText> + {_(msg`From @${sanitizedAuthor}`)} + </Layout.Header.SubtitleText> + )} + </Layout.Header.Content> + <Layout.Header.Slot> + <Button + label={_(msg`Share`)} + size="small" + variant="ghost" + color="primary" + shape="round" + onPress={onShare} + hitSlop={HITSLOP_10} + style={[{right: -3}]}> + <ButtonIcon icon={Share} size="md" /> + </Button> + </Layout.Header.Slot> + </Layout.Header.Outer> <Pager onPageSelected={onPageSelected} renderTabBar={props => ( - <CenteredView - sideBorders={true} - // @ts-ignore web only - style={ - isWeb - ? { - position: isWeb ? 'sticky' : '', - top: 0, - zIndex: 1, - } - : undefined - }> + <Layout.Center style={web([a.sticky, a.z_10, {top: 0}])}> <TabBar items={sections.map(section => section.title)} {...props} /> - </CenteredView> + </Layout.Center> )} initialPage={0}> {sections.map((section, i) => ( diff --git a/src/screens/Messages/ChatList.tsx b/src/screens/Messages/ChatList.tsx index 4f2bd251f..1a87a2ac5 100644 --- a/src/screens/Messages/ChatList.tsx +++ b/src/screens/Messages/ChatList.tsx @@ -16,8 +16,6 @@ import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const' import {useMessagesEventBus} from '#/state/messages/events' import {useListConvosQuery} from '#/state/queries/messages/list-converations' import {List} from '#/view/com/util/List' -import {ViewHeader} from '#/view/com/util/ViewHeader' -import {CenteredView} from '#/view/com/util/Views' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {DialogControlProps, useDialogControl} from '#/components/Dialog' @@ -49,7 +47,6 @@ export function MessagesScreen({navigation, route}: Props) { const {_} = useLingui() const t = useTheme() const newChatControl = useDialogControl() - const {gtMobile} = useBreakpoints() const pushToConversation = route.params?.pushToConversation // Whenever we have `pushToConversation` set, it means we pressed a notification for a chat without being on @@ -81,21 +78,6 @@ export function MessagesScreen({navigation, route}: Props) { }, [messagesBus, isActive]), ) - const renderButton = useCallback(() => { - return ( - <Link - to="/messages/settings" - label={_(msg`Chat settings`)} - size="small" - variant="ghost" - color="secondary" - shape="square" - style={[a.justify_center]}> - <SettingsSlider size="md" style={[t.atoms.text_contrast_medium]} /> - </Link> - ) - }, [_, t]) - const initialNumToRender = useInitialNumToRender({minItemHeight: 80}) const [isPTRing, setIsPTRing] = useState(false) @@ -144,28 +126,11 @@ export function MessagesScreen({navigation, route}: Props) { [navigation], ) - const onNavigateToSettings = useCallback(() => { - navigation.navigate('MessagesSettings') - }, [navigation]) - if (conversations.length < 1) { return ( <Layout.Screen> - <CenteredView sideBorders={gtMobile} style={[a.h_full_vh]}> - {gtMobile ? ( - <DesktopHeader - newChatControl={newChatControl} - onNavigateToSettings={onNavigateToSettings} - /> - ) : ( - <ViewHeader - title={_(msg`Messages`)} - renderButton={renderButton} - showBorder - canGoBack={false} - /> - )} - + <Header newChatControl={newChatControl} /> + <Layout.Center> {isLoading ? ( <View style={[a.align_center, a.pt_3xl, web({paddingTop: '10vh'})]}> <Loader size="xl" /> @@ -227,7 +192,7 @@ export function MessagesScreen({navigation, route}: Props) { )} </> )} - </CenteredView> + </Layout.Center> {!isLoading && !isError && ( <NewChat onNewChat={onNewChat} control={newChatControl} /> @@ -238,14 +203,7 @@ export function MessagesScreen({navigation, route}: Props) { return ( <Layout.Screen testID="messagesScreen"> - {!gtMobile && ( - <ViewHeader - title={_(msg`Messages`)} - renderButton={renderButton} - showBorder - canGoBack={false} - /> - )} + <Header newChatControl={newChatControl} /> <NewChat onNewChat={onNewChat} control={newChatControl} /> <List data={conversations} @@ -254,12 +212,6 @@ export function MessagesScreen({navigation, route}: Props) { refreshing={isPTRing} onRefresh={onRefresh} onEndReached={onEndReached} - ListHeaderComponent={ - <DesktopHeader - newChatControl={newChatControl} - onNavigateToSettings={onNavigateToSettings} - /> - } ListFooterComponent={ <ListFooter isFetchingNextPage={isFetchingNextPage} @@ -276,67 +228,65 @@ export function MessagesScreen({navigation, route}: Props) { windowSize={11} // @ts-ignore our .web version only -sfn desktopFixedHeight + sideBorders={false} /> </Layout.Screen> ) } -function DesktopHeader({ - newChatControl, - onNavigateToSettings, -}: { - newChatControl: DialogControlProps - onNavigateToSettings: () => void -}) { - const t = useTheme() +function Header({newChatControl}: {newChatControl: DialogControlProps}) { const {_} = useLingui() - const {gtMobile, gtTablet} = useBreakpoints() + const {gtMobile} = useBreakpoints() - if (!gtMobile) { - return null - } + const settingsLink = ( + <Link + to="/messages/settings" + label={_(msg`Chat settings`)} + size="small" + variant="ghost" + color="secondary" + shape="square" + style={[a.justify_center]}> + <ButtonIcon icon={SettingsSlider} size="md" /> + </Link> + ) return ( - <View - style={[ - t.atoms.bg, - a.flex_row, - a.align_center, - a.justify_between, - a.gap_lg, - a.px_lg, - a.pr_md, - a.py_sm, - a.border_b, - t.atoms.border_contrast_low, - ]}> - <Text style={[a.text_2xl, a.font_bold]}> - <Trans>Messages</Trans> - </Text> - <View style={[a.flex_row, a.align_center, a.gap_sm]}> - <Button - label={_(msg`Message settings`)} - color="secondary" - size="small" - variant="ghost" - shape="square" - onPress={onNavigateToSettings}> - <SettingsSlider size="md" style={[t.atoms.text_contrast_medium]} /> - </Button> - {gtTablet && ( - <Button - label={_(msg`New chat`)} - color="primary" - size="small" - variant="solid" - onPress={newChatControl.open}> - <ButtonIcon icon={Plus} position="left" /> - <ButtonText> - <Trans>New chat</Trans> - </ButtonText> - </Button> - )} - </View> - </View> + <Layout.Header.Outer> + {gtMobile ? ( + <> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Messages</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + + <View style={[a.flex_row, a.align_center, a.gap_sm]}> + {settingsLink} + <Button + label={_(msg`New chat`)} + color="primary" + size="small" + variant="solid" + onPress={newChatControl.open}> + <ButtonIcon icon={Plus} position="left" /> + <ButtonText> + <Trans>New chat</Trans> + </ButtonText> + </Button> + </View> + </> + ) : ( + <> + <Layout.Header.MenuButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Messages</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot>{settingsLink}</Layout.Header.Slot> + </> + )} + </Layout.Header.Outer> ) } diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx index a2157d2b9..b8b0bfe0d 100644 --- a/src/screens/Messages/Conversation.tsx +++ b/src/screens/Messages/Conversation.tsx @@ -17,7 +17,6 @@ import {useCurrentConvoId} from '#/state/messages/current-convo-id' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useProfileQuery} from '#/state/queries/profile' import {useSetMinimalShellMode} from '#/state/shell' -import {CenteredView} from '#/view/com/util/Views' import {MessagesList} from '#/screens/Messages/components/MessagesList' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {useDialogControl} from '#/components/Dialog' @@ -97,7 +96,7 @@ function Inner() { if (convoState.status === ConvoStatus.Error) { return ( - <CenteredView style={[a.flex_1]} sideBorders> + <Layout.Center style={[a.flex_1]}> <MessagesListHeader /> <Error title={_(msg`Something went wrong`)} @@ -105,12 +104,12 @@ function Inner() { onRetry={() => convoState.error.retry()} sideBorders={false} /> - </CenteredView> + </Layout.Center> ) } return ( - <CenteredView style={[a.flex_1]} sideBorders> + <Layout.Center style={[a.flex_1]}> {!readyToShow && <MessagesListHeader />} <View style={[a.flex_1]}> {moderationOpts && recipient ? ( @@ -140,7 +139,7 @@ function Inner() { </View> )} </View> - </CenteredView> + </Layout.Center> ) } diff --git a/src/screens/Messages/Settings.tsx b/src/screens/Messages/Settings.tsx index 50b1c4cc9..f37e7a9ba 100644 --- a/src/screens/Messages/Settings.tsx +++ b/src/screens/Messages/Settings.tsx @@ -10,8 +10,6 @@ import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declarat import {useProfileQuery} from '#/state/queries/profile' import {useSession} from '#/state/session' import * as Toast from '#/view/com/util/Toast' -import {ViewHeader} from '#/view/com/util/ViewHeader' -import {ScrollView} from '#/view/com/util/Views' import {atoms as a} from '#/alf' import {Admonition} from '#/components/Admonition' import {Divider} from '#/components/Divider' @@ -57,8 +55,16 @@ export function MessagesSettingsScreen({}: Props) { return ( <Layout.Screen testID="messagesSettingsScreen"> - <ScrollView stickyHeaderIndices={[0]}> - <ViewHeader title={_(msg`Chat Settings`)} showOnDesktop showBorder /> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Chat Settings</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> + <Layout.Content> <View style={[a.p_lg, a.gap_md]}> <Text style={[a.text_lg, a.font_bold]}> <Trans>Allow new messages from</Trans> @@ -142,7 +148,7 @@ export function MessagesSettingsScreen({}: Props) { </> )} </View> - </ScrollView> + </Layout.Content> </Layout.Screen> ) } diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx index 5f340cd56..6b4dd06bc 100644 --- a/src/screens/Moderation/index.tsx +++ b/src/screens/Moderation/index.tsx @@ -1,6 +1,5 @@ -import React from 'react' +import {Fragment, useCallback} from 'react' import {Linking, View} from 'react-native' -import {useSafeAreaFrame} from 'react-native-safe-area-context' import {LABELS} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -19,8 +18,6 @@ import { import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities' import {useSetMinimalShellMode} from '#/state/shell' import {ViewHeader} from '#/view/com/util/ViewHeader' -import {CenteredView} from '#/view/com/util/Views' -import {ScrollView} from '#/view/com/util/Views' import {atoms as a, useBreakpoints, useTheme, ViewStyleProp} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -37,6 +34,7 @@ import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Perso import * as LabelingService from '#/components/LabelingServiceCard' import * as Layout from '#/components/Layout' import {InlineLinkText, Link} from '#/components/Link' +import {ListMaybePlaceholder} from '#/components/Lists' import {Loader} from '#/components/Loader' import {GlobalLabelPreference} from '#/components/moderation/LabelPreference' import {Text} from '#/components/Typography' @@ -75,35 +73,22 @@ function ErrorState({error}: {error: string}) { export function ModerationScreen( _props: NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>, ) { - const t = useTheme() const {_} = useLingui() const { isLoading: isPreferencesLoading, error: preferencesError, data: preferences, } = usePreferencesQuery() - const {gtMobile} = useBreakpoints() - const {height} = useSafeAreaFrame() const isLoading = isPreferencesLoading const error = preferencesError return ( <Layout.Screen testID="moderationScreen"> - <CenteredView - testID="moderationScreen" - style={[ - t.atoms.border_contrast_low, - t.atoms.bg, - {minHeight: height}, - ...(gtMobile ? [a.border_l, a.border_r] : []), - ]}> - <ViewHeader title={_(msg`Moderation`)} showOnDesktop /> - + <ViewHeader title={_(msg`Moderation`)} showOnDesktop /> + <Layout.Content> {isLoading ? ( - <View style={[a.w_full, a.align_center, a.pt_2xl]}> - <Loader size="xl" fill={t.atoms.text.color} /> - </View> + <ListMaybePlaceholder isLoading={true} sideBorders={false} /> ) : error || !preferences ? ( <ErrorState error={ @@ -114,7 +99,7 @@ export function ModerationScreen( ) : ( <ModerationScreenInner preferences={preferences} /> )} - </CenteredView> + </Layout.Content> </Layout.Screen> ) } @@ -169,7 +154,7 @@ export function ModerationScreenInner({ } = useMyLabelersQuery() useFocusEffect( - React.useCallback(() => { + useCallback(() => { setMinimalShellMode(false) }, [setMinimalShellMode]), ) @@ -183,7 +168,7 @@ export function ModerationScreenInner({ const ageNotSet = !preferences.userAge const isUnderage = (preferences.userAge || 0) < 18 - const onToggleAdultContentEnabled = React.useCallback( + const onToggleAdultContentEnabled = useCallback( async (selected: boolean) => { try { await setAdultContentPref({ @@ -201,13 +186,7 @@ export function ModerationScreenInner({ const disabledOnIOS = isIOS && !adultContentEnabled return ( - <ScrollView - contentContainerStyle={[ - a.border_0, - a.pt_2xl, - a.px_lg, - gtMobile && a.px_2xl, - ]}> + <View style={[a.pt_2xl, a.px_lg, gtMobile && a.px_2xl]}> <Text style={[a.text_md, a.font_bold, a.pb_md, t.atoms.text_contrast_high]}> <Trans>Moderation tools</Trans> @@ -420,7 +399,7 @@ export function ModerationScreenInner({ <View style={[a.rounded_sm, t.atoms.bg_contrast_25]}> {labelers.map((labeler, i) => { return ( - <React.Fragment key={labeler.creator.did}> + <Fragment key={labeler.creator.did}> {i !== 0 && <Divider />} <LabelingService.Link labeler={labeler}> {state => ( @@ -457,12 +436,12 @@ export function ModerationScreenInner({ </LabelingService.Outer> )} </LabelingService.Link> - </React.Fragment> + </Fragment> ) })} </View> )} - <View style={{height: 200}} /> - </ScrollView> + <View style={{height: 150}} /> + </View> ) } diff --git a/src/screens/Onboarding/Layout.tsx b/src/screens/Onboarding/Layout.tsx index 54821532c..059cdfd5c 100644 --- a/src/screens/Onboarding/Layout.tsx +++ b/src/screens/Onboarding/Layout.tsx @@ -1,13 +1,11 @@ import React from 'react' -import {View} from 'react-native' -import Animated from 'react-native-reanimated' +import {ScrollView, View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {isWeb} from '#/platform/detection' import {useOnboardingDispatch} from '#/state/shell' -import {ScrollView} from '#/view/com/util/Views' import {Context} from '#/screens/Onboarding/state' import { atoms as a, @@ -36,7 +34,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) { const {gtMobile} = useBreakpoints() const onboardDispatch = useOnboardingDispatch() const {state, dispatch} = React.useContext(Context) - const scrollview = React.useRef<Animated.ScrollView>(null) + const scrollview = React.useRef<ScrollView>(null) const prevActiveStep = React.useRef<string>(state.activeStep) React.useEffect(() => { diff --git a/src/screens/Post/PostLikedBy.tsx b/src/screens/Post/PostLikedBy.tsx index 6fc485f34..d35d33243 100644 --- a/src/screens/Post/PostLikedBy.tsx +++ b/src/screens/Post/PostLikedBy.tsx @@ -5,13 +5,10 @@ import {useFocusEffect} from '@react-navigation/native' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {makeRecordUri} from '#/lib/strings/url-helpers' -import {isWeb} from '#/platform/detection' import {useSetMinimalShellMode} from '#/state/shell' import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy' import {ViewHeader} from '#/view/com/util/ViewHeader' -import {CenteredView} from '#/view/com/util/Views' import * as Layout from '#/components/Layout' -import {ListHeaderDesktop} from '#/components/Lists' type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'> export const PostLikedByScreen = ({route}: Props) => { @@ -28,11 +25,8 @@ export const PostLikedByScreen = ({route}: Props) => { return ( <Layout.Screen> - <CenteredView sideBorders={true}> - <ListHeaderDesktop title={_(msg`Liked By`)} /> - <ViewHeader title={_(msg`Liked By`)} showBorder={!isWeb} /> - <PostLikedByComponent uri={uri} /> - </CenteredView> + <ViewHeader title={_(msg`Liked By`)} /> + <PostLikedByComponent uri={uri} /> </Layout.Screen> ) } diff --git a/src/screens/Post/PostQuotes.tsx b/src/screens/Post/PostQuotes.tsx index 71dd8ad8d..2cd6be879 100644 --- a/src/screens/Post/PostQuotes.tsx +++ b/src/screens/Post/PostQuotes.tsx @@ -11,7 +11,6 @@ import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuot import {ViewHeader} from '#/view/com/util/ViewHeader' import {CenteredView} from '#/view/com/util/Views' import * as Layout from '#/components/Layout' -import {ListHeaderDesktop} from '#/components/Lists' type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostQuotes'> export const PostQuotesScreen = ({route}: Props) => { @@ -29,7 +28,6 @@ export const PostQuotesScreen = ({route}: Props) => { return ( <Layout.Screen> <CenteredView sideBorders={true}> - <ListHeaderDesktop title={_(msg`Quotes`)} /> <ViewHeader title={_(msg`Quotes`)} showBorder={!isWeb} /> <PostQuotesComponent uri={uri} /> </CenteredView> diff --git a/src/screens/Post/PostRepostedBy.tsx b/src/screens/Post/PostRepostedBy.tsx index c1e8b2987..304e70808 100644 --- a/src/screens/Post/PostRepostedBy.tsx +++ b/src/screens/Post/PostRepostedBy.tsx @@ -11,7 +11,6 @@ import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/ import {ViewHeader} from '#/view/com/util/ViewHeader' import {CenteredView} from '#/view/com/util/Views' import * as Layout from '#/components/Layout' -import {ListHeaderDesktop} from '#/components/Lists' type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'> export const PostRepostedByScreen = ({route}: Props) => { @@ -29,7 +28,6 @@ export const PostRepostedByScreen = ({route}: Props) => { return ( <Layout.Screen> <CenteredView sideBorders={true}> - <ListHeaderDesktop title={_(msg`Reposted By`)} /> <ViewHeader title={_(msg`Reposted By`)} showBorder={!isWeb} /> <PostRepostedByComponent uri={uri} /> </CenteredView> diff --git a/src/screens/Profile/KnownFollowers.tsx b/src/screens/Profile/KnownFollowers.tsx index 7e396c350..d6dd15c69 100644 --- a/src/screens/Profile/KnownFollowers.tsx +++ b/src/screens/Profile/KnownFollowers.tsx @@ -15,14 +15,22 @@ import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' import {List} from '#/view/com/util/List' import {ViewHeader} from '#/view/com/util/ViewHeader' import * as Layout from '#/components/Layout' -import { - ListFooter, - ListHeaderDesktop, - ListMaybePlaceholder, -} from '#/components/Lists' +import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' -function renderItem({item}: {item: AppBskyActorDefs.ProfileViewBasic}) { - return <ProfileCardWithFollowBtn key={item.did} profile={item} /> +function renderItem({ + item, + index, +}: { + item: AppBskyActorDefs.ProfileViewBasic + index: number +}) { + return ( + <ProfileCardWithFollowBtn + key={item.did} + profile={item} + noBorder={index === 0} + /> + ) } function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) { @@ -93,6 +101,7 @@ export const ProfileKnownFollowersScreen = ({route}: Props) => { if (followers.length < 1) { return ( <Layout.Screen> + <ViewHeader title={_(msg`Followers you know`)} /> <ListMaybePlaceholder isLoading={isDidLoading || isFollowersLoading} isError={isError} @@ -100,6 +109,8 @@ export const ProfileKnownFollowersScreen = ({route}: Props) => { emptyMessage={_(msg`You don't follow any users who follow @${name}.`)} errorMessage={cleanError(resolveError || error)} onRetry={isError ? refetch : undefined} + topBorder={false} + sideBorders={false} /> </Layout.Screen> ) @@ -116,9 +127,6 @@ export const ProfileKnownFollowersScreen = ({route}: Props) => { onRefresh={onRefresh} onEndReached={onEndReached} onEndReachedThreshold={4} - ListHeaderComponent={ - <ListHeaderDesktop title={_(msg`Followers you know`)} /> - } ListFooterComponent={ <ListFooter isFetchingNextPage={isFetchingNextPage} @@ -130,6 +138,7 @@ export const ProfileKnownFollowersScreen = ({route}: Props) => { desktopFixedHeight initialNumToRender={initialNumToRender} windowSize={11} + sideBorders={false} /> </Layout.Screen> ) diff --git a/src/screens/Profile/Sections/Labels.tsx b/src/screens/Profile/Sections/Labels.tsx index 67c827d90..6c76d7b15 100644 --- a/src/screens/Profile/Sections/Labels.tsx +++ b/src/screens/Profile/Sections/Labels.tsx @@ -15,10 +15,11 @@ import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation' import {useScrollHandlers} from '#/lib/ScrollContext' import {isNative} from '#/platform/detection' import {ListRef} from '#/view/com/util/List' -import {CenteredView, ScrollView} from '#/view/com/util/Views' +import {ScrollView} from '#/view/com/util/Views' import {atoms as a, useTheme} from '#/alf' import {Divider} from '#/components/Divider' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' +import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' import {LabelerLabelPreference} from '#/components/moderation/LabelPreference' import {Text} from '#/components/Typography' @@ -75,7 +76,7 @@ export const ProfileLabelsSection = React.forwardRef< }, [isFocused, scrollElRef, setScrollViewTag]) return ( - <CenteredView style={{flex: 1, minHeight}} sideBorders> + <Layout.Center style={{flex: 1, minHeight}}> {isLabelerLoading ? ( <View style={[a.w_full, a.align_center]}> <Loader size="xl" /> @@ -95,7 +96,7 @@ export const ProfileLabelsSection = React.forwardRef< headerHeight={headerHeight} /> )} - </CenteredView> + </Layout.Center> ) }) diff --git a/src/screens/Settings/AboutSettings.tsx b/src/screens/Settings/AboutSettings.tsx index 8019a20f9..02976bb3c 100644 --- a/src/screens/Settings/AboutSettings.tsx +++ b/src/screens/Settings/AboutSettings.tsx @@ -21,7 +21,15 @@ export function AboutSettingsScreen({}: Props) { return ( <Layout.Screen> - <Layout.Header title={_(msg`About`)} /> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>About</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> <Layout.Content> <SettingsList.Container> <SettingsList.LinkItem diff --git a/src/screens/Settings/AccessibilitySettings.tsx b/src/screens/Settings/AccessibilitySettings.tsx index 6ab0131d9..ee26697d2 100644 --- a/src/screens/Settings/AccessibilitySettings.tsx +++ b/src/screens/Settings/AccessibilitySettings.tsx @@ -39,7 +39,15 @@ export function AccessibilitySettingsScreen({}: Props) { return ( <Layout.Screen> - <Layout.Header title={_(msg`Accessibility`)} /> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Accessibility</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> <Layout.Content> <SettingsList.Container> <SettingsList.Group contentContainerStyle={[a.gap_sm]}> diff --git a/src/screens/Settings/AccountSettings.tsx b/src/screens/Settings/AccountSettings.tsx index 2495a0f2f..634c9d3f7 100644 --- a/src/screens/Settings/AccountSettings.tsx +++ b/src/screens/Settings/AccountSettings.tsx @@ -38,7 +38,15 @@ export function AccountSettingsScreen({}: Props) { return ( <Layout.Screen> - <Layout.Header title={_(msg`Account`)} /> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Account</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> <Layout.Content> <SettingsList.Container> <SettingsList.Item> diff --git a/src/screens/Settings/AppIconSettings.tsx b/src/screens/Settings/AppIconSettings.tsx index 1dd87d45f..18fcd5e30 100644 --- a/src/screens/Settings/AppIconSettings.tsx +++ b/src/screens/Settings/AppIconSettings.tsx @@ -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,7 +20,15 @@ export function AppIconSettingsScreen({}: Props) { return ( <Layout.Screen> - <Layout.Header title={_('App Icon')} /> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>App Icon</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> <Layout.Content contentContainerStyle={[a.py_2xl, a.px_xl, {paddingBottom: 100}]}> <Text style={[a.text_lg, a.font_heavy]}>Defaults</Text> diff --git a/src/screens/Settings/AppPasswords.tsx b/src/screens/Settings/AppPasswords.tsx index 1ea0bd1b3..630d26ba7 100644 --- a/src/screens/Settings/AppPasswords.tsx +++ b/src/screens/Settings/AppPasswords.tsx @@ -44,7 +44,15 @@ export function AppPasswordsScreen({}: Props) { return ( <Layout.Screen testID="AppPasswordsScreen"> - <Layout.Header title={_(msg`App Passwords`)} /> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>App Passwords</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> <Layout.Content> {error ? ( <ErrorScreen diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx index 82c4ef97e..48c4a2d85 100644 --- a/src/screens/Settings/AppearanceSettings.tsx +++ b/src/screens/Settings/AppearanceSettings.tsx @@ -79,7 +79,15 @@ export function AppearanceSettingsScreen({}: Props) { return ( <LayoutAnimationConfig skipExiting skipEntering> <Layout.Screen testID="preferencesThreadsScreen"> - <Layout.Header title={_(msg`Appearance`)} /> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Appearance</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> <Layout.Content> <SettingsList.Container> <AppearanceToggleButtonGroup diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx index b3fb8c174..17f8fa506 100644 --- a/src/screens/Settings/ContentAndMediaSettings.tsx +++ b/src/screens/Settings/ContentAndMediaSettings.tsx @@ -32,7 +32,15 @@ export function ContentAndMediaSettingsScreen({}: Props) { return ( <Layout.Screen> - <Layout.Header title={_(msg`Content and Media`)} /> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Content & Media</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> <Layout.Content> <SettingsList.Container> <SettingsList.LinkItem diff --git a/src/screens/Settings/ExternalMediaPreferences.tsx b/src/screens/Settings/ExternalMediaPreferences.tsx index f7e081429..ae859295f 100644 --- a/src/screens/Settings/ExternalMediaPreferences.tsx +++ b/src/screens/Settings/ExternalMediaPreferences.tsx @@ -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,10 +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.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>External Media Preferences</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> <Layout.Content> <SettingsList.Container> <SettingsList.Item> diff --git a/src/screens/Settings/FollowingFeedPreferences.tsx b/src/screens/Settings/FollowingFeedPreferences.tsx index 089491dd0..ea9455ab1 100644 --- a/src/screens/Settings/FollowingFeedPreferences.tsx +++ b/src/screens/Settings/FollowingFeedPreferences.tsx @@ -46,7 +46,15 @@ export function FollowingFeedPreferencesScreen({}: Props) { return ( <Layout.Screen testID="followingFeedPreferencesScreen"> - <Layout.Header title={_(msg`Following Feed Preferences`)} /> + <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.Slot /> + </Layout.Header.Outer> <Layout.Content> <SettingsList.Container> <SettingsList.Item> diff --git a/src/screens/Settings/LanguageSettings.tsx b/src/screens/Settings/LanguageSettings.tsx index a44e2fcec..096f92566 100644 --- a/src/screens/Settings/LanguageSettings.tsx +++ b/src/screens/Settings/LanguageSettings.tsx @@ -64,7 +64,15 @@ export function LanguageSettingsScreen({}: Props) { return ( <Layout.Screen testID="PreferencesLanguagesScreen"> - <Layout.Header title={_(msg`Languages`)} /> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Languages</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> <Layout.Content> <SettingsList.Container> <SettingsList.Group iconInset={false}> diff --git a/src/screens/Settings/NotificationSettings.tsx b/src/screens/Settings/NotificationSettings.tsx index c5f7078c4..1c77b3148 100644 --- a/src/screens/Settings/NotificationSettings.tsx +++ b/src/screens/Settings/NotificationSettings.tsx @@ -33,7 +33,15 @@ export function NotificationSettingsScreen({}: Props) { return ( <Layout.Screen> - <Layout.Header title={_(msg`Notification Settings`)} /> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Notification Settings</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> <Layout.Content> {isQueryError ? ( <Error diff --git a/src/screens/Settings/PrivacyAndSecuritySettings.tsx b/src/screens/Settings/PrivacyAndSecuritySettings.tsx index d695f830d..870ece4bf 100644 --- a/src/screens/Settings/PrivacyAndSecuritySettings.tsx +++ b/src/screens/Settings/PrivacyAndSecuritySettings.tsx @@ -29,7 +29,15 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) { return ( <Layout.Screen> - <Layout.Header title={_(msg`Privacy and Security`)} /> + <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.Slot /> + </Layout.Header.Outer> <Layout.Content> <SettingsList.Container> <SettingsList.Item> diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index 126a1bc88..7a4ad6f20 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -73,7 +73,15 @@ export function SettingsScreen({}: Props) { return ( <Layout.Screen> - <Layout.Header title={_(msg`Settings`)} /> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Settings</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> <Layout.Content> <SettingsList.Container> <View diff --git a/src/screens/Settings/ThreadPreferences.tsx b/src/screens/Settings/ThreadPreferences.tsx index d29daa58b..b1547e495 100644 --- a/src/screens/Settings/ThreadPreferences.tsx +++ b/src/screens/Settings/ThreadPreferences.tsx @@ -38,7 +38,15 @@ export function ThreadPreferencesScreen({}: Props) { return ( <Layout.Screen testID="threadPreferencesScreen"> - <Layout.Header title={_(msg`Thread Preferences`)} /> + <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> <Layout.Content> <SettingsList.Container> <SettingsList.Group> diff --git a/src/screens/SignupQueued.tsx b/src/screens/SignupQueued.tsx index ed261f29e..f1c36a69c 100644 --- a/src/screens/SignupQueued.tsx +++ b/src/screens/SignupQueued.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {Modal, View} from 'react-native' +import {Modal, ScrollView, View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {StatusBar} from 'expo-status-bar' import {msg, plural, Trans} from '@lingui/macro' @@ -9,7 +9,6 @@ import {logger} from '#/logger' import {isIOS, isWeb} from '#/platform/detection' import {isSignupQueued, useAgent, useSessionApi} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' -import {ScrollView} from '#/view/com/util/Views' import {Logo} from '#/view/icons/Logo' import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx index b0d71b929..b42b753e3 100644 --- a/src/screens/StarterPack/Wizard/index.tsx +++ b/src/screens/StarterPack/Wizard/index.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {Keyboard, TouchableOpacity, View} from 'react-native' +import {Keyboard, View} from 'react-native' import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {Image} from 'expo-image' @@ -10,13 +10,12 @@ import { ModerationOpts, } from '@atproto/api' import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect, useNavigation} from '@react-navigation/native' import {NativeStackScreenProps} from '@react-navigation/native-stack' -import {HITSLOP_10, STARTER_PACK_MAX_SIZE} from '#/lib/constants' +import {STARTER_PACK_MAX_SIZE} from '#/lib/constants' import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' @@ -29,7 +28,7 @@ import { parseStarterPackUri, } from '#/lib/strings/starter-pack' import {logger} from '#/logger' -import {isAndroid, isNative, isWeb} from '#/platform/detection' +import {isNative} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useAllListMembersQuery} from '#/state/queries/list-members' import {useProfileQuery} from '#/state/queries/profile' @@ -147,7 +146,6 @@ function WizardInner({ }) { const navigation = useNavigation<NavigationProp>() const {_} = useLingui() - const t = useTheme() const setMinimalShellMode = useSetMinimalShellMode() const [state, dispatch] = useWizardState() const {currentAccount} = useSession() @@ -283,45 +281,24 @@ function WizardInner({ return ( <CenteredView style={[a.flex_1]} sideBorders> - <View - style={[ - a.flex_row, - a.pb_sm, - a.px_md, - a.border_b, - t.atoms.border_contrast_medium, - a.gap_sm, - a.justify_between, - a.align_center, - isAndroid && a.pt_sm, - isWeb && [a.py_md], - ]}> - <View style={[{width: 65}]}> - <TouchableOpacity - testID="viewHeaderDrawerBtn" - hitSlop={HITSLOP_10} - accessibilityRole="button" - accessibilityLabel={_(msg`Back`)} - accessibilityHint={_(msg`Go back to the previous step`)} - onPress={() => { - if (state.currentStep === 'Details') { - navigation.pop() - } else { - dispatch({type: 'Back'}) - } - }}> - <FontAwesomeIcon - size={18} - icon="angle-left" - color={t.atoms.text.color} - /> - </TouchableOpacity> - </View> - <Text style={[a.flex_1, a.font_bold, a.text_lg, a.text_center]}> - {currUiStrings.header} - </Text> - <View style={[{width: 65}]} /> - </View> + <Layout.Header.Outer> + <Layout.Header.BackButton + label={_(msg`Back`)} + accessibilityHint={_(msg`Go back to the previous step`)} + onPress={evt => { + if (state.currentStep !== 'Details') { + evt.preventDefault() + dispatch({type: 'Back'}) + } + }} + /> + <Layout.Header.Content> + <Layout.Header.TitleText> + {currUiStrings.header} + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> <Container> {state.currentStep === 'Details' ? ( @@ -463,17 +440,17 @@ function Footer({ <Trans> <Text style={[a.font_bold, textStyles]}>You</Text> and <Text> </Text> - <Text style={[a.font_bold, textStyles]}> + <Text style={[a.font_bold, textStyles]} emoji> {getName(items[1] /* [0] is self, skip it */)}{' '} </Text> are included in your starter pack </Trans> ) : items.length > 2 ? ( <Trans context="profiles"> - <Text style={[a.font_bold, textStyles]}> + <Text style={[a.font_bold, textStyles]} emoji> {getName(items[1] /* [0] is self, skip it */)},{' '} </Text> - <Text style={[a.font_bold, textStyles]}> + <Text style={[a.font_bold, textStyles]} emoji> {getName(items[2])},{' '} </Text> and{' '} @@ -504,29 +481,29 @@ function Footer({ { items.length === 1 ? ( <Trans> - <Text style={[a.font_bold, textStyles]}> + <Text style={[a.font_bold, textStyles]} emoji> {getName(items[0])} </Text>{' '} is included in your starter pack </Trans> ) : items.length === 2 ? ( <Trans> - <Text style={[a.font_bold, textStyles]}> + <Text style={[a.font_bold, textStyles]} emoji> {getName(items[0])} </Text>{' '} and <Text> </Text> - <Text style={[a.font_bold, textStyles]}> + <Text style={[a.font_bold, textStyles]} emoji> {getName(items[1])}{' '} </Text> are included in your starter pack </Trans> ) : items.length > 2 ? ( <Trans context="feeds"> - <Text style={[a.font_bold, textStyles]}> + <Text style={[a.font_bold, textStyles]} emoji> {getName(items[0])},{' '} </Text> - <Text style={[a.font_bold, textStyles]}> + <Text style={[a.font_bold, textStyles]} emoji> {getName(items[1])},{' '} </Text> and{' '} diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 44e90a551..fa5a620bf 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -108,7 +108,7 @@ export function FeedPage({ }, [scrollToTop, feed, queryClient, setHasNew]) return ( - <View testID={testID} style={s.h100pct}> + <View testID={testID}> <MainScrollProvider> <FeedFeedbackProvider value={feedFeedback}> <Feed diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx index bdfc2c7ff..1dc67b6c3 100644 --- a/src/view/com/home/HomeHeaderLayout.web.tsx +++ b/src/view/com/home/HomeHeaderLayout.web.tsx @@ -1,26 +1,27 @@ import React from 'react' -import {StyleSheet, View} from 'react-native' +import {View} from 'react-native' import Animated from 'react-native-reanimated' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useKawaiiMode} from '#/state/preferences/kawaii' import {useSession} from '#/state/session' import {useShellLayout} from '#/state/shell/shell-layout' +import {HomeHeaderLayoutMobile} from '#/view/com/home/HomeHeaderLayoutMobile' import {Logo} from '#/view/icons/Logo' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, useBreakpoints, useGutterStyles, useTheme} from '#/alf' +import {ButtonIcon} from '#/components/Button' import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag' +import * as Layout from '#/components/Layout' import {Link} from '#/components/Link' -import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile' export function HomeHeaderLayout(props: { children: React.ReactNode tabBarAnchor: JSX.Element | null | undefined }) { - const {isMobile} = useWebMediaQueries() - if (isMobile) { + const {gtMobile} = useBreakpoints() + if (!gtMobile) { return <HomeHeaderLayoutMobile {...props} /> } else { return <HomeHeaderLayoutDesktopAndTablet {...props} /> @@ -40,98 +41,43 @@ function HomeHeaderLayoutDesktopAndTablet({ const {hasSession} = useSession() const {_} = useLingui() const kawaii = useKawaiiMode() + const gutter = useGutterStyles() return ( <> {hasSession && ( - <View - style={[ - a.relative, - a.flex_row, - a.justify_end, - a.align_center, - a.pt_lg, - a.px_md, - a.pb_2xs, - t.atoms.bg, - t.atoms.border_contrast_low, - styles.bar, - kawaii && {paddingTop: 22, paddingBottom: 16}, - ]}> + <Layout.Center> <View - style={[ - a.absolute, - a.inset_0, - a.pt_lg, - a.m_auto, - kawaii && {paddingTop: 4, paddingBottom: 0}, - { - width: kawaii ? 84 : 28, - }, - ]}> - <Logo width={kawaii ? 60 : 28} /> + style={[a.flex_row, a.align_center, a.pt_md, gutter, t.atoms.bg]}> + <View style={{width: 34}} /> + <View style={[a.flex_1, a.align_center, a.justify_center]}> + <Logo width={kawaii ? 60 : 28} /> + </View> + <Link + to="/feeds" + hitSlop={10} + label={_(msg`View your feeds and explore more`)} + size="small" + variant="ghost" + color="secondary" + shape="square" + style={[a.justify_center]}> + <ButtonIcon icon={FeedsIcon} size="lg" /> + </Link> </View> - - <Link - to="/feeds" - hitSlop={10} - label={_(msg`View your feeds and explore more`)} - size="small" - variant="ghost" - color="secondary" - shape="square" - style={[ - a.justify_center, - { - marginTop: -4, - }, - ]}> - <FeedsIcon size="md" fill={t.atoms.text_contrast_medium.color} /> - </Link> - </View> + </Layout.Center> )} {tabBarAnchor} - <Animated.View - onLayout={e => { - headerHeight.set(e.nativeEvent.layout.height) - }} - style={[ - t.atoms.bg, - t.atoms.border_contrast_low, - styles.bar, - styles.tabBar, - headerMinimalShellTransform, - ]}> - {children} - </Animated.View> + <Layout.Center + style={[a.sticky, a.z_10, a.align_center, t.atoms.bg, {top: 0}]}> + <Animated.View + onLayout={e => { + headerHeight.set(e.nativeEvent.layout.height) + }} + style={[headerMinimalShellTransform]}> + {children} + </Animated.View> + </Layout.Center> </> ) } - -const styles = StyleSheet.create({ - bar: { - // @ts-ignore Web only - left: 'calc(50% - 300px)', - width: 600, - borderLeftWidth: 1, - borderRightWidth: 1, - }, - topBar: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingHorizontal: 18, - paddingTop: 16, - paddingBottom: 8, - }, - tabBar: { - // @ts-ignore Web only - position: 'sticky', - top: 0, - flexDirection: 'column', - alignItems: 'center', - borderLeftWidth: 1, - borderRightWidth: 1, - zIndex: 1, - }, -}) diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx index 832396092..e48c2cc89 100644 --- a/src/view/com/home/HomeHeaderLayoutMobile.tsx +++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx @@ -1,25 +1,22 @@ import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' +import {View} from 'react-native' import Animated from 'react-native-reanimated' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {HITSLOP_10} from '#/lib/constants' +import {PressableScale} from '#/lib/custom-animations/PressableScale' +import {useHaptics} from '#/lib/haptics' import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform' -import {usePalette} from '#/lib/hooks/usePalette' -import {isWeb} from '#/platform/detection' +import {emitSoftReset} from '#/state/events' import {useSession} from '#/state/session' -import {useSetDrawerOpen} from '#/state/shell/drawer-open' import {useShellLayout} from '#/state/shell/shell-layout' import {Logo} from '#/view/icons/Logo' -import {atoms} from '#/alf' -import {useTheme} from '#/alf' -import {atoms as a} from '#/alf' -import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette' +import {atoms as a, useTheme} from '#/alf' +import {ButtonIcon} from '#/components/Button' import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag' -import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' +import * as Layout from '#/components/Layout' import {Link} from '#/components/Link' -import {IS_DEV} from '#/env' export function HomeHeaderLayoutMobile({ children, @@ -28,58 +25,50 @@ export function HomeHeaderLayoutMobile({ tabBarAnchor: JSX.Element | null | undefined }) { const t = useTheme() - const pal = usePalette('default') const {_} = useLingui() - const setDrawerOpen = useSetDrawerOpen() const {headerHeight} = useShellLayout() const headerMinimalShellTransform = useMinimalShellHeaderTransform() const {hasSession} = useSession() - - const onPressAvi = React.useCallback(() => { - setDrawerOpen(true) - }, [setDrawerOpen]) + const playHaptic = useHaptics() return ( <Animated.View - style={[pal.border, styles.tabBar, headerMinimalShellTransform]} + style={[ + a.fixed, + a.z_10, + t.atoms.bg, + { + top: 0, + left: 0, + right: 0, + }, + headerMinimalShellTransform, + ]} onLayout={e => { headerHeight.set(e.nativeEvent.layout.height) }}> - <View style={[pal.view, styles.topBar]}> - <View style={[{width: 100}]}> - <TouchableOpacity - testID="viewHeaderDrawerBtn" - onPress={onPressAvi} - accessibilityRole="button" - accessibilityLabel={_(msg`Open navigation`)} - accessibilityHint={_( - msg`Access profile and other navigation links`, - )} - hitSlop={HITSLOP_10}> - <Menu size="lg" fill={t.atoms.text_contrast_medium.color} /> - </TouchableOpacity> - </View> - <View> - <Logo width={30} /> + <Layout.Header.Outer noBottomBorder> + <Layout.Header.Slot> + <Layout.Header.MenuButton /> + </Layout.Header.Slot> + + <View style={[a.flex_1, a.align_center]}> + <PressableScale + targetScale={0.9} + onPress={() => { + emitSoftReset() + }} + onPressIn={() => { + playHaptic('Heavy') + }} + onPressOut={() => { + playHaptic('Light') + }}> + <Logo width={30} /> + </PressableScale> </View> - <View - style={[ - atoms.flex_row, - atoms.justify_end, - atoms.align_center, - atoms.gap_md, - {width: 100}, - ]}> - {IS_DEV && ( - <> - <Link - label="View storybook" - to="/sys/debug" - testID="storybookBtn"> - <ColorPalette size="md" /> - </Link> - </> - )} + + <Layout.Header.Slot> {hasSession && ( <Link testID="viewHeaderHomeFeedPrefsBtn" @@ -93,40 +82,15 @@ export function HomeHeaderLayoutMobile({ style={[ a.justify_center, { - marginTop: 2, - marginRight: -6, + marginRight: -Layout.BUTTON_VISUAL_ALIGNMENT_OFFSET, }, ]}> - <FeedsIcon size="lg" fill={t.atoms.text_contrast_medium.color} /> + <ButtonIcon icon={FeedsIcon} size="lg" /> </Link> )} - </View> - </View> + </Layout.Header.Slot> + </Layout.Header.Outer> {children} </Animated.View> ) } - -const styles = StyleSheet.create({ - tabBar: { - // @ts-ignore web-only - position: isWeb ? 'fixed' : 'absolute', - zIndex: 1, - left: 0, - right: 0, - top: 0, - flexDirection: 'column', - }, - topBar: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingHorizontal: 16, - paddingVertical: 5, - width: '100%', - minHeight: 46, - }, - title: { - fontSize: 21, - }, -}) diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx index f9b147b29..f6b6223ce 100644 --- a/src/view/com/lightbox/Lightbox.web.tsx +++ b/src/view/com/lightbox/Lightbox.web.tsx @@ -15,8 +15,8 @@ import { } from '@fortawesome/react-native-fontawesome' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {RemoveScrollBar} from 'react-remove-scroll-bar' -import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {colors, s} from '#/lib/styles' import {useLightbox, useLightboxControls} from '#/state/lightbox' @@ -28,7 +28,6 @@ export function Lightbox() { const {activeLightbox} = useLightbox() const {closeLightbox} = useLightboxControls() const isActive = !!activeLightbox - useWebBodyScrollLock(isActive) if (!isActive) { return null @@ -37,11 +36,14 @@ export function Lightbox() { const initialIndex = activeLightbox.index const imgs = activeLightbox.images return ( - <LightboxInner - imgs={imgs} - initialIndex={initialIndex} - onClose={closeLightbox} - /> + <> + <RemoveScrollBar /> + <LightboxInner + imgs={imgs} + initialIndex={initialIndex} + onClose={closeLightbox} + /> + </> ) } diff --git a/src/view/com/lists/MyLists.tsx b/src/view/com/lists/MyLists.tsx index 363dd100d..17327fd9a 100644 --- a/src/view/com/lists/MyLists.tsx +++ b/src/view/com/lists/MyLists.tsx @@ -15,7 +15,6 @@ import {usePalette} from '#/lib/hooks/usePalette' import {cleanError} from '#/lib/strings/errors' import {s} from '#/lib/styles' import {logger} from '#/logger' -import {isWeb} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {MyListsFilter, useMyListsQuery} from '#/state/queries/my-lists' import {EmptyState} from '#/view/com/util/EmptyState' @@ -110,7 +109,7 @@ export function MyLists({ ) : ( <View style={[ - (index !== 0 || isWeb) && a.border_t, + index !== 0 && a.border_t, t.atoms.border_contrast_low, a.px_lg, a.py_lg, @@ -141,8 +140,6 @@ export function MyLists({ } contentContainerStyle={[s.contentContainer]} removeClippedSubviews={true} - // @ts-ignore our .web version only -prf - desktopFixedHeight /> )} </View> @@ -160,8 +157,8 @@ export function MyLists({ onRefresh={onRefresh} contentContainerStyle={[s.contentContainer]} removeClippedSubviews={true} - // @ts-ignore our .web version only -prf desktopFixedHeight + sideBorders={false} /> )} </View> diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 8d93c21b4..0c49c8771 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -1,8 +1,8 @@ import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' +import {RemoveScrollBar} from 'react-remove-scroll-bar' import {usePalette} from '#/lib/hooks/usePalette' -import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import type {Modal as ModalIface} from '#/state/modals' import {useModalControls, useModals} from '#/state/modals' @@ -22,7 +22,6 @@ import * as VerifyEmailModal from './VerifyEmail' export function ModalsContainer() { const {isModalActive, activeModals} = useModals() - useWebBodyScrollLock(isModalActive) if (!isModalActive) { return null @@ -30,6 +29,7 @@ export function ModalsContainer() { return ( <> + <RemoveScrollBar /> {activeModals.map((modal, i) => ( <Modal key={`modal-${i}`} modal={modal} /> ))} diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index bd39ddd84..9871455a1 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -10,7 +10,6 @@ import {useLingui} from '@lingui/react' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {cleanError} from '#/lib/strings/errors' import {s} from '#/lib/styles' import {logger} from '#/logger' @@ -22,7 +21,6 @@ import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {List, ListRef} from '#/view/com/util/List' import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' -import {CenteredView} from '#/view/com/util/Views' import {FeedItem} from './FeedItem' const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} @@ -46,7 +44,6 @@ export function Feed({ const [isPTRing, setIsPTRing] = React.useState(false) const pal = usePalette('default') - const {isTabletOrMobile} = useWebMediaQueries() const {_} = useLingui() const moderationOpts = useModerationOpts() @@ -133,11 +130,7 @@ export function Feed({ ) } else if (item === LOADING_ITEM) { return ( - <View - style={[ - pal.border, - !isTabletOrMobile && {borderTopWidth: StyleSheet.hairlineWidth}, - ]}> + <View style={[pal.border]}> <NotificationFeedLoadingPlaceholder /> </View> ) @@ -146,11 +139,11 @@ export function Feed({ <FeedItem item={item} moderationOpts={moderationOpts!} - hideTopBorder={index === 0 && isTabletOrMobile} + hideTopBorder={index === 0} /> ) }, - [moderationOpts, isTabletOrMobile, _, onPressRetryLoadMore, pal.border], + [moderationOpts, _, onPressRetryLoadMore, pal.border], ) const FeedFooter = React.useCallback( @@ -168,12 +161,10 @@ export function Feed({ return ( <View style={s.hContentRegion}> {error && ( - <CenteredView> - <ErrorMessage - message={cleanError(error)} - onPressTryAgain={onPressTryAgain} - /> - </CenteredView> + <ErrorMessage + message={cleanError(error)} + onPressTryAgain={onPressTryAgain} + /> )} <List testID="notifsFeed" diff --git a/src/view/com/pager/PagerWithHeader.web.tsx b/src/view/com/pager/PagerWithHeader.web.tsx index 13c723f47..3335532b3 100644 --- a/src/view/com/pager/PagerWithHeader.web.tsx +++ b/src/view/com/pager/PagerWithHeader.web.tsx @@ -1,10 +1,10 @@ import * as React from 'react' -import {ScrollView, StyleSheet, View} from 'react-native' +import {ScrollView, View} from 'react-native' import {useAnimatedRef} from 'react-native-reanimated' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager' +import {atoms as a, web} from '#/alf' +import * as Layout from '#/components/Layout' import {ListMethods} from '../util/List' import {TabBar} from './TabBar' @@ -121,30 +121,19 @@ let PagerTabBar = ({ onSelect?: (index: number) => void tabBarAnchor?: JSX.Element | null | undefined }): React.ReactNode => { - const pal = usePalette('default') - const {isMobile} = useWebMediaQueries() return ( <> - <View - style={[ - !isMobile && styles.headerContainerDesktop, - pal.border, - !isHeaderReady && styles.loadingHeader, - ]}> - {renderHeader?.()} - </View> + <Layout.Center>{renderHeader?.()}</Layout.Center> {tabBarAnchor} - <View - style={[ - styles.tabBarContainer, - isMobile - ? styles.tabBarContainerMobile - : styles.tabBarContainerDesktop, - pal.border, + <Layout.Center + style={web([ + a.sticky, + a.z_10, { + top: 0, display: isHeaderReady ? undefined : 'none', }, - ]}> + ])}> <TabBar testID={testID} items={items} @@ -154,7 +143,7 @@ let PagerTabBar = ({ dragProgress={undefined as any /* native-only */} dragState={undefined as any /* native-only */} /> - </View> + </Layout.Center> </> ) } @@ -180,33 +169,6 @@ function PagerItem({ }) } -const styles = StyleSheet.create({ - headerContainerDesktop: { - marginHorizontal: 'auto', - width: 600, - borderLeftWidth: 1, - borderRightWidth: 1, - }, - tabBarContainer: { - // @ts-ignore web-only - position: 'sticky', - top: 0, - zIndex: 1, - }, - tabBarContainerDesktop: { - marginHorizontal: 'auto', - width: 600, - borderLeftWidth: 1, - borderRightWidth: 1, - }, - tabBarContainerMobile: { - paddingHorizontal: 0, - }, - loadingHeader: { - borderColor: 'transparent', - }, -}) - function toArray<T>(v: T | T[]): T[] { if (Array.isArray(v)) { return v diff --git a/src/view/com/post-thread/PostLikedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx index 4c0d973a9..b9051a9c6 100644 --- a/src/view/com/post-thread/PostLikedBy.tsx +++ b/src/view/com/post-thread/PostLikedBy.tsx @@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' -import {isWeb} from '#/platform/detection' import {useLikedByQuery} from '#/state/queries/post-liked-by' import {useResolveUriQuery} from '#/state/queries/resolve-uri' import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' @@ -18,7 +17,7 @@ function renderItem({item, index}: {item: GetLikes.Like; index: number}) { <ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} - noBorder={index === 0 && !isWeb} + noBorder={index === 0} /> ) } @@ -88,6 +87,7 @@ export function PostLikedBy({uri}: {uri: string}) { )} errorMessage={cleanError(resolveError || error)} sideBorders={false} + topBorder={false} /> ) } @@ -108,7 +108,6 @@ export function PostLikedBy({uri}: {uri: string}) { onRetry={fetchNextPage} /> } - // @ts-ignore our .web version only -prf desktopFixedHeight initialNumToRender={initialNumToRender} windowSize={11} diff --git a/src/view/com/post-thread/PostQuotes.tsx b/src/view/com/post-thread/PostQuotes.tsx index 10a51166c..a22000b96 100644 --- a/src/view/com/post-thread/PostQuotes.tsx +++ b/src/view/com/post-thread/PostQuotes.tsx @@ -11,7 +11,6 @@ import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' -import {isWeb} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {usePostQuotesQuery} from '#/state/queries/post-quotes' import {useResolveUriQuery} from '#/state/queries/resolve-uri' @@ -30,7 +29,7 @@ function renderItem({ } index: number }) { - return <Post post={item.post} hideTopBorder={index === 0 && !isWeb} /> + return <Post post={item.post} hideTopBorder={index === 0} /> } function keyExtractor(item: { diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx index dfaa69780..2143bd9c2 100644 --- a/src/view/com/post-thread/PostRepostedBy.tsx +++ b/src/view/com/post-thread/PostRepostedBy.tsx @@ -12,8 +12,20 @@ import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' import {List} from '#/view/com/util/List' import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' -function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) { - return <ProfileCardWithFollowBtn key={item.did} profile={item} /> +function renderItem({ + item, + index, +}: { + item: ActorDefs.ProfileViewBasic + index: number +}) { + return ( + <ProfileCardWithFollowBtn + key={item.did} + profile={item} + noBorder={index === 0} + /> + ) } function keyExtractor(item: ActorDefs.ProfileViewBasic) { diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index a10149395..0cdccff59 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -32,7 +32,6 @@ import {usePreferencesQuery} from '#/state/queries/preferences' import {useSession} from '#/state/session' import {useComposerControls} from '#/state/shell' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' -import {CenteredView} from '#/view/com/util/Views' import {atoms as a, useTheme} from '#/alf' import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' import {Text} from '#/components/Typography' @@ -484,7 +483,7 @@ export function PostThread({uri}: {uri: string | undefined}) { } return ( - <CenteredView style={[a.flex_1]} sideBorders={true}> + <> {showHeader && ( <ViewHeader title={_(msg({message: `Post`, context: 'description'}))} @@ -531,7 +530,7 @@ export function PostThread({uri}: {uri: string | undefined}) { {isMobile && canReply && hasSession && ( <MobileComposePrompt onPressReply={onPressReply} /> )} - </CenteredView> + </> ) } diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx index 60a7a5e31..3c0476929 100644 --- a/src/view/com/profile/ProfileFollowers.tsx +++ b/src/view/com/profile/ProfileFollowers.tsx @@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' -import {isWeb} from '#/platform/detection' import {useProfileFollowersQuery} from '#/state/queries/profile-followers' import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useSession} from '#/state/session' @@ -25,7 +24,7 @@ function renderItem({ <ProfileCardWithFollowBtn key={item.did} profile={item} - noBorder={index === 0 && !isWeb} + noBorder={index === 0} /> ) } diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx index 572b0b9f4..1cd65c74c 100644 --- a/src/view/com/profile/ProfileFollows.tsx +++ b/src/view/com/profile/ProfileFollows.tsx @@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' -import {isWeb} from '#/platform/detection' import {useProfileFollowsQuery} from '#/state/queries/profile-follows' import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useSession} from '#/state/session' @@ -25,7 +24,7 @@ function renderItem({ <ProfileCardWithFollowBtn key={item.did} profile={item} - noBorder={index === 0 && !isWeb} + noBorder={index === 0} /> ) } diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx index 0e25fe5e6..cd11611a8 100644 --- a/src/view/com/profile/ProfileSubpageHeader.tsx +++ b/src/view/com/profile/ProfileSubpageHeader.tsx @@ -1,29 +1,24 @@ import React from 'react' -import {Pressable, StyleSheet, View} from 'react-native' +import {Pressable, View} from 'react-native' import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' -import {BACK_HITSLOP} from '#/lib/constants' import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {makeProfileLink} from '#/lib/routes/links' import {NavigationProp} from '#/lib/routes/types' import {sanitizeHandle} from '#/lib/strings/handles' -import {isNative} from '#/platform/detection' import {emitSoftReset} from '#/state/events' import {useLightboxControls} from '#/state/lightbox' -import {useSetDrawerOpen} from '#/state/shell' -import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' +import {TextLink} from '#/view/com/util/Link' +import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' +import {Text} from '#/view/com/util/text/Text' +import {UserAvatar, UserAvatarType} from '#/view/com/util/UserAvatar' import {StarterPack} from '#/components/icons/StarterPack' -import {TextLink} from '../util/Link' -import {LoadingPlaceholder} from '../util/LoadingPlaceholder' -import {Text} from '../util/text/Text' -import {UserAvatar, UserAvatarType} from '../util/UserAvatar' -import {CenteredView} from '../util/Views' +import * as Layout from '#/components/Layout' export function ProfileSubpageHeader({ isLoading, @@ -48,7 +43,6 @@ export function ProfileSubpageHeader({ | undefined avatarType: UserAvatarType | 'starter-pack' }>) { - const setDrawerOpen = useSetDrawerOpen() const navigation = useNavigation<NavigationProp>() const {_} = useLingui() const {isMobile} = useWebMediaQueries() @@ -57,18 +51,6 @@ export function ProfileSubpageHeader({ const canGoBack = navigation.canGoBack() const aviRef = useHandleRef() - const onPressBack = React.useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } else { - navigation.navigate('Home') - } - }, [navigation]) - - const onPressMenu = React.useCallback(() => { - setDrawerOpen(true) - }, [setDrawerOpen]) - const _openLightbox = React.useCallback( (uri: string, thumbRect: MeasuredDimensions | null) => { openLightbox({ @@ -106,42 +88,17 @@ export function ProfileSubpageHeader({ }, [_openLightbox, avatar, aviRef]) return ( - <CenteredView style={pal.view}> - {isMobile && ( - <View - style={[ - { - flexDirection: 'row', - alignItems: 'center', - borderBottomWidth: StyleSheet.hairlineWidth, - paddingTop: isNative ? 0 : 8, - paddingBottom: 8, - paddingHorizontal: isMobile ? 12 : 14, - }, - pal.border, - ]}> - <Pressable - testID="headerDrawerBtn" - onPress={canGoBack ? onPressBack : onPressMenu} - hitSlop={BACK_HITSLOP} - style={canGoBack ? styles.backBtn : styles.backBtnWide} - accessibilityRole="button" - accessibilityLabel={canGoBack ? 'Back' : 'Menu'} - accessibilityHint=""> - {canGoBack ? ( - <FontAwesomeIcon - size={18} - icon="angle-left" - style={[styles.backIcon, pal.text]} - /> - ) : ( - <Menu size="lg" style={[{marginTop: 4}, pal.textLight]} /> - )} - </Pressable> - <View style={{flex: 1}} /> - {children} - </View> - )} + <> + <Layout.Header.Outer> + {canGoBack ? ( + <Layout.Header.BackButton /> + ) : ( + <Layout.Header.MenuButton /> + )} + <Layout.Header.Content /> + {children} + </Layout.Header.Outer> + <View style={{ flexDirection: 'row', @@ -206,31 +163,7 @@ export function ProfileSubpageHeader({ </Text> )} </View> - {!isMobile && ( - <View - style={{ - flexDirection: 'row', - alignItems: 'center', - }}> - {children} - </View> - )} </View> - </CenteredView> + </> ) } - -const styles = StyleSheet.create({ - backBtn: { - width: 20, - height: 30, - }, - backBtnWide: { - width: 20, - height: 30, - marginRight: 4, - }, - backIcon: { - marginTop: 6, - }, -}) diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index f112d2d0a..18f7d6fa7 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -4,10 +4,9 @@ import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook import {batchedUpdates} from '#/lib/batchedUpdates' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useScrollHandlers} from '#/lib/ScrollContext' import {addStyle} from '#/lib/styles' +import * as Layout from '#/components/Layout' export type ListMethods = any // TODO: Better types. export type ListProps<ItemT> = Omit< @@ -24,6 +23,9 @@ export type ListProps<ItemT> = Omit< desktopFixedHeight?: number | boolean // Web only prop to contain the scroll to the container rather than the window disableFullWindowScroll?: boolean + /** + * @deprecated Should be using Layout components + */ sideBorders?: boolean } export type ListRef = React.MutableRefObject<any | null> // TODO: Better types. @@ -56,20 +58,11 @@ function ListImpl<ItemT>( renderItem, extraData, style, - sideBorders = true, ...props }: ListProps<ItemT>, ref: React.Ref<ListMethods>, ) { const contextScrollHandlers = useScrollHandlers() - const pal = usePalette('default') - const {isMobile} = useWebMediaQueries() - if (!isMobile) { - contentContainerStyle = addStyle( - contentContainerStyle, - styles.containerScroll, - ) - } const isEmpty = !data || data.length === 0 @@ -326,53 +319,53 @@ function ListImpl<ItemT>( styles.parentTreeVisibilityDetector } /> - <View - ref={containerRef} - style={[ - !isMobile && sideBorders && styles.sideBorders, - contentContainerStyle, - desktopFixedHeight ? styles.minHeightViewport : null, - pal.border, - ]}> - <Visibility - root={disableFullWindowScroll ? nativeRef : null} - onVisibleChange={handleAboveTheFoldVisibleChange} - style={[styles.aboveTheFoldDetector, {height: headerOffset}]} - /> - {onStartReached && !isEmpty && ( - <EdgeVisibility - root={disableFullWindowScroll ? nativeRef : null} - onVisibleChange={onHeadVisibilityChange} - topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'} - containerRef={containerRef} - /> - )} - {headerComponent} - {isEmpty - ? emptyComponent - : (data as Array<ItemT>)?.map((item, index) => { - const key = keyExtractor!(item, index) - return ( - <Row<ItemT> - key={key} - item={item} - index={index} - renderItem={renderItem} - extraData={extraData} - onItemSeen={onItemSeen} - /> - ) - })} - {onEndReached && !isEmpty && ( - <EdgeVisibility + <Layout.Center> + <View + ref={containerRef} + style={[ + contentContainerStyle, + desktopFixedHeight ? styles.minHeightViewport : null, + ]}> + <Visibility root={disableFullWindowScroll ? nativeRef : null} - onVisibleChange={onTailVisibilityChange} - bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'} - containerRef={containerRef} + onVisibleChange={handleAboveTheFoldVisibleChange} + style={[styles.aboveTheFoldDetector, {height: headerOffset}]} /> - )} - {footerComponent} - </View> + {onStartReached && !isEmpty && ( + <EdgeVisibility + root={disableFullWindowScroll ? nativeRef : null} + onVisibleChange={onHeadVisibilityChange} + topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'} + containerRef={containerRef} + /> + )} + {headerComponent} + {isEmpty + ? emptyComponent + : (data as Array<ItemT>)?.map((item, index) => { + const key = keyExtractor!(item, index) + return ( + <Row<ItemT> + key={key} + item={item} + index={index} + renderItem={renderItem} + extraData={extraData} + onItemSeen={onItemSeen} + /> + ) + })} + {onEndReached && !isEmpty && ( + <EdgeVisibility + root={disableFullWindowScroll ? nativeRef : null} + onVisibleChange={onTailVisibilityChange} + bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'} + containerRef={containerRef} + /> + )} + {footerComponent} + </View> + </Layout.Center> </View> ) } @@ -558,16 +551,6 @@ export const List = memo(React.forwardRef(ListImpl)) as <ItemT>( // https://stackoverflow.com/questions/7944460/detect-safari-browser const styles = StyleSheet.create({ - sideBorders: { - borderLeftWidth: 1, - borderRightWidth: 1, - }, - containerScroll: { - width: '100%', - maxWidth: 600, - marginLeft: 'auto', - marginRight: 'auto', - }, minHeightViewport: { // @ts-ignore web only minHeight: '100vh', diff --git a/src/view/com/util/LoadingScreen.tsx b/src/view/com/util/LoadingScreen.tsx index 5d2aeb38f..1086c9d17 100644 --- a/src/view/com/util/LoadingScreen.tsx +++ b/src/view/com/util/LoadingScreen.tsx @@ -1,14 +1,17 @@ import {ActivityIndicator, View} from 'react-native' import {s} from '#/lib/styles' -import {CenteredView} from './Views' +import * as Layout from '#/components/Layout' +/** + * @deprecated use Layout compoenents directly + */ export function LoadingScreen() { return ( - <CenteredView> + <Layout.Content> <View style={s.p20}> <ActivityIndicator size="large" /> </View> - </CenteredView> + </Layout.Content> ) } diff --git a/src/view/com/util/SimpleViewHeader.tsx b/src/view/com/util/SimpleViewHeader.tsx deleted file mode 100644 index 78b66a929..000000000 --- a/src/view/com/util/SimpleViewHeader.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react' -import { - StyleProp, - StyleSheet, - TouchableOpacity, - View, - ViewStyle, -} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {useNavigation} from '@react-navigation/native' - -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {NavigationProp} from '#/lib/routes/types' -import {isWeb} from '#/platform/detection' -import {useSetDrawerOpen} from '#/state/shell' -import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' -import {CenteredView} from './Views' - -const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} - -export function SimpleViewHeader({ - showBackButton = true, - style, - children, -}: React.PropsWithChildren<{ - showBackButton?: boolean - style?: StyleProp<ViewStyle> -}>) { - const pal = usePalette('default') - const setDrawerOpen = useSetDrawerOpen() - const navigation = useNavigation<NavigationProp>() - const {isMobile} = useWebMediaQueries() - const canGoBack = navigation.canGoBack() - - const onPressBack = React.useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } else { - navigation.navigate('Home') - } - }, [navigation]) - - const onPressMenu = React.useCallback(() => { - setDrawerOpen(true) - }, [setDrawerOpen]) - - const Container = isMobile ? View : CenteredView - return ( - <Container - style={[ - styles.header, - isMobile && styles.headerMobile, - isWeb && styles.headerWeb, - pal.view, - style, - ]}> - {showBackButton ? ( - <TouchableOpacity - testID="viewHeaderDrawerBtn" - onPress={canGoBack ? onPressBack : onPressMenu} - hitSlop={BACK_HITSLOP} - style={canGoBack ? styles.backBtn : styles.backBtnWide} - accessibilityRole="button" - accessibilityLabel={canGoBack ? 'Back' : 'Menu'} - accessibilityHint=""> - {canGoBack ? ( - <FontAwesomeIcon - size={18} - icon="angle-left" - style={[styles.backIcon, pal.text]} - /> - ) : ( - <Menu size="lg" style={[{marginTop: 4}, pal.textLight]} /> - )} - </TouchableOpacity> - ) : null} - {children} - </Container> - ) -} - -const styles = StyleSheet.create({ - header: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 18, - paddingVertical: 12, - width: '100%', - }, - headerMobile: { - paddingHorizontal: 12, - paddingVertical: 10, - }, - headerWeb: { - // @ts-ignore web-only - position: 'sticky', - top: 0, - zIndex: 1, - }, - backBtn: { - width: 30, - height: 30, - }, - backBtnWide: { - width: 30, - height: 30, - paddingLeft: 4, - marginRight: 4, - }, - backIcon: { - marginTop: 6, - }, -}) diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index 1d4cf8ff0..2d413f782 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -1,271 +1,27 @@ -import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' -import Animated from 'react-native-reanimated' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useNavigation} from '@react-navigation/native' - -import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {NavigationProp} from '#/lib/routes/types' -import {useSetDrawerOpen} from '#/state/shell' -import {useTheme} from '#/alf' -import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' -import {Text} from './text/Text' -import {CenteredView} from './Views' - -const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} +import {Header} from '#/components/Layout' +/** + * Legacy ViewHeader component. Use Layout.Header going forward. + * + * @deprecated + */ export function ViewHeader({ title, - subtitle, - canGoBack, - showBackButton = true, - hideOnScroll, - showOnDesktop, - showBorder, renderButton, }: { title: string subtitle?: string - canGoBack?: boolean - showBackButton?: boolean - hideOnScroll?: boolean showOnDesktop?: boolean showBorder?: boolean renderButton?: () => JSX.Element }) { - const pal = usePalette('default') - const {_} = useLingui() - const setDrawerOpen = useSetDrawerOpen() - const navigation = useNavigation<NavigationProp>() - const {isDesktop, isTablet} = useWebMediaQueries() - const t = useTheme() - - const onPressBack = React.useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } else { - navigation.navigate('Home') - } - }, [navigation]) - - const onPressMenu = React.useCallback(() => { - setDrawerOpen(true) - }, [setDrawerOpen]) - - if (isDesktop) { - if (showOnDesktop) { - return ( - <DesktopWebHeader - title={title} - subtitle={subtitle} - renderButton={renderButton} - showBorder={showBorder} - /> - ) - } - return null - } else { - if (typeof canGoBack === 'undefined') { - canGoBack = navigation.canGoBack() - } - - return ( - <Container hideOnScroll={hideOnScroll || false} showBorder={showBorder}> - <View style={{flex: 1}}> - <View style={{flexDirection: 'row', alignItems: 'center'}}> - {showBackButton ? ( - <TouchableOpacity - testID="viewHeaderDrawerBtn" - onPress={canGoBack ? onPressBack : onPressMenu} - hitSlop={BACK_HITSLOP} - style={canGoBack ? styles.backBtn : styles.backBtnWide} - accessibilityRole="button" - accessibilityLabel={canGoBack ? _(msg`Back`) : _(msg`Menu`)} - accessibilityHint={ - canGoBack ? '' : _(msg`Access navigation links and settings`) - }> - {canGoBack ? ( - <FontAwesomeIcon - size={18} - icon="angle-left" - style={[styles.backIcon, pal.text]} - /> - ) : !isTablet ? ( - <Menu size="lg" style={[{marginTop: 3}, pal.textLight]} /> - ) : null} - </TouchableOpacity> - ) : null} - <View style={styles.titleContainer} pointerEvents="none"> - <Text emoji type="title" style={[pal.text, styles.title]}> - {title} - </Text> - </View> - {renderButton ? ( - renderButton() - ) : showBackButton ? ( - <View style={canGoBack ? styles.backBtn : styles.backBtnWide} /> - ) : null} - </View> - {subtitle ? ( - <View - style={[styles.titleContainer, {marginTop: -3}]} - pointerEvents="none"> - <Text - style={[ - pal.text, - styles.subtitle, - t.atoms.text_contrast_medium, - ]}> - {subtitle} - </Text> - </View> - ) : undefined} - </View> - </Container> - ) - } -} - -function DesktopWebHeader({ - title, - subtitle, - renderButton, - showBorder = true, -}: { - title: string - subtitle?: string - renderButton?: () => JSX.Element - showBorder?: boolean -}) { - const pal = usePalette('default') - const t = useTheme() - return ( - <CenteredView - style={[ - styles.header, - styles.desktopHeader, - pal.border, - { - borderBottomWidth: showBorder ? StyleSheet.hairlineWidth : 0, - }, - {display: 'flex', flexDirection: 'column'}, - ]}> - <View> - <View style={styles.titleContainer} pointerEvents="none"> - <Text type="title-lg" style={[pal.text, styles.title]}> - {title} - </Text> - </View> - {renderButton?.()} - </View> - {subtitle ? ( - <View> - <View style={[styles.titleContainer]} pointerEvents="none"> - <Text - style={[ - pal.text, - styles.subtitleDesktop, - t.atoms.text_contrast_medium, - ]}> - {subtitle} - </Text> - </View> - </View> - ) : null} - </CenteredView> - ) -} - -function Container({ - children, - hideOnScroll, - showBorder, -}: { - children: React.ReactNode - hideOnScroll: boolean - showBorder?: boolean -}) { - const pal = usePalette('default') - const headerMinimalShellTransform = useMinimalShellHeaderTransform() - - if (!hideOnScroll) { - return ( - <View - style={[ - styles.header, - pal.view, - pal.border, - showBorder && styles.border, - ]}> - {children} - </View> - ) - } return ( - <Animated.View - style={[ - styles.header, - styles.headerFloating, - pal.view, - pal.border, - headerMinimalShellTransform, - showBorder && styles.border, - ]}> - {children} - </Animated.View> + <Header.Outer> + <Header.BackButton /> + <Header.Content> + <Header.TitleText>{title}</Header.TitleText> + </Header.Content> + <Header.Slot>{renderButton?.() ?? null}</Header.Slot> + </Header.Outer> ) } - -const styles = StyleSheet.create({ - header: { - flexDirection: 'row', - paddingHorizontal: 12, - paddingVertical: 6, - width: '100%', - }, - headerFloating: { - position: 'absolute', - top: 0, - width: '100%', - }, - desktopHeader: { - paddingVertical: 12, - maxWidth: 600, - marginLeft: 'auto', - marginRight: 'auto', - }, - border: { - borderBottomWidth: StyleSheet.hairlineWidth, - }, - titleContainer: { - marginLeft: 'auto', - marginRight: 'auto', - alignItems: 'center', - }, - title: { - fontWeight: '600', - }, - subtitle: { - fontSize: 13, - }, - subtitleDesktop: { - fontSize: 15, - }, - backBtn: { - width: 30, - height: 30, - }, - backBtnWide: { - width: 30, - height: 30, - paddingLeft: 4, - marginRight: 4, - }, - backIcon: { - marginTop: 6, - }, -}) diff --git a/src/view/com/util/Views.tsx b/src/view/com/util/Views.tsx index 0d3f63794..c9ba0728c 100644 --- a/src/view/com/util/Views.tsx +++ b/src/view/com/util/Views.tsx @@ -15,9 +15,16 @@ export type FlatList_INTERNAL<ItemT = any> = Omit< FlatListComponent<ItemT, FlatListPropsWithLayout<ItemT>>, 'CellRendererComponent' > + +/** + * @deprecated use `Layout` components + */ export const ScrollView = Animated.ScrollView export type ScrollView = typeof Animated.ScrollView +/** + * @deprecated use `Layout` components + */ export const CenteredView = forwardRef< View, React.PropsWithChildren< diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx index 1f030b408..e64b0ce9a 100644 --- a/src/view/com/util/Views.web.tsx +++ b/src/view/com/util/Views.web.tsx @@ -31,10 +31,12 @@ interface AddedProps { desktopFixedHeight?: boolean | number } +/** + * @deprecated use `Layout` components + */ export const CenteredView = React.forwardRef(function CenteredView( { style, - sideBorders, topBorder, ...props }: React.PropsWithChildren< @@ -47,13 +49,6 @@ export const CenteredView = React.forwardRef(function CenteredView( if (!isMobile) { style = addStyle(style, styles.container) } - if (sideBorders && !isMobile) { - style = addStyle(style, { - borderLeftWidth: StyleSheet.hairlineWidth, - borderRightWidth: StyleSheet.hairlineWidth, - }) - style = addStyle(style, pal.border) - } if (topBorder) { style = addStyle(style, { borderTopWidth: 1, @@ -75,7 +70,6 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>( >, ref: React.Ref<FlatList<ItemT>>, ) { - const pal = usePalette('default') const {isMobile} = useWebMediaQueries() if (!isMobile) { contentContainerStyle = addStyle( @@ -123,11 +117,7 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>( return ( <Animated.FlatList ref={ref} - contentContainerStyle={[ - styles.contentContainer, - contentContainerStyle, - pal.border, - ]} + contentContainerStyle={[styles.contentContainer, contentContainerStyle]} style={style} contentOffset={contentOffset} {...props} @@ -135,12 +125,13 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>( ) }) +/** + * @deprecated use `Layout` components + */ export const ScrollView = React.forwardRef(function ScrollViewImpl( {contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>, ref: React.Ref<Animated.ScrollView>, ) { - const pal = usePalette('default') - const {isMobile} = useWebMediaQueries() if (!isMobile) { contentContainerStyle = addStyle( @@ -150,11 +141,7 @@ export const ScrollView = React.forwardRef(function ScrollViewImpl( } return ( <Animated.ScrollView - contentContainerStyle={[ - styles.contentContainer, - contentContainerStyle, - pal.border, - ]} + contentContainerStyle={[styles.contentContainer, contentContainerStyle]} // @ts-ignore something is wrong with the reanimated types -prf ref={ref} {...props} @@ -164,8 +151,6 @@ export const ScrollView = React.forwardRef(function ScrollViewImpl( const styles = StyleSheet.create({ contentContainer: { - borderLeftWidth: StyleSheet.hairlineWidth, - borderRightWidth: StyleSheet.hairlineWidth, // @ts-ignore web only minHeight: '100vh', }, diff --git a/src/view/com/util/error/ErrorScreen.tsx b/src/view/com/util/error/ErrorScreen.tsx index b66f43789..846f4d295 100644 --- a/src/view/com/util/error/ErrorScreen.tsx +++ b/src/view/com/util/error/ErrorScreen.tsx @@ -36,7 +36,9 @@ export function ErrorScreen({ return ( <> - {showHeader && isMobile && <ViewHeader title="Error" showBorder />} + {showHeader && isMobile && ( + <ViewHeader title={_(msg`Error`)} showBorder /> + )} <CenteredView testID={testID} style={[styles.outer, pal.view]}> <View style={styles.errorIconContainer}> <View diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx index 406f11792..0dcf1f016 100644 --- a/src/view/screens/Feeds.tsx +++ b/src/view/screens/Feeds.tsx @@ -24,14 +24,13 @@ import {useSetMinimalShellMode} from '#/state/shell' import {useComposerControls} from '#/state/shell/composer' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {FAB} from '#/view/com/util/fab/FAB' -import {TextLink} from '#/view/com/util/Link' import {List, ListMethods} from '#/view/com/util/List' import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {Text} from '#/view/com/util/text/Text' -import {ViewHeader} from '#/view/com/util/ViewHeader' import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed' import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType' import {atoms as a, useTheme} from '#/alf' +import {ButtonIcon} from '#/components/Button' import {Divider} from '#/components/Divider' import * as FeedCard from '#/components/FeedCard' import {SearchInput} from '#/components/forms/SearchInput' @@ -40,7 +39,9 @@ import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass' import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle' +import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2' import * as Layout from '#/components/Layout' +import {Link} from '#/components/Link' import * as ListCard from '#/components/ListCard' type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'> @@ -102,7 +103,7 @@ type FlatlistSlice = export function FeedsScreen(_props: Props) { const pal = usePalette('default') const {openComposer} = useComposerControls() - const {isMobile, isTabletOrDesktop} = useWebMediaQueries() + const {isMobile} = useWebMediaQueries() const [query, setQuery] = React.useState('') const [isPTR, setIsPTR] = React.useState(false) const { @@ -374,22 +375,6 @@ export function FeedsScreen(_props: Props) { isUserSearching, ]) - const renderHeaderBtn = React.useCallback(() => { - return ( - <View style={styles.headerBtnGroup}> - <TextLink - testID="editFeedsBtn" - type="lg-medium" - href="/settings/saved-feeds" - accessibilityLabel={_(msg`Edit My Feeds`)} - accessibilityHint="" - text={_(msg`Edit`)} - style={[pal.link, a.pr_xs]} - /> - </View> - ) - }, [pal, _]) - const searchBarIndex = items.findIndex( item => item.type === 'popularFeedsHeader', ) @@ -430,36 +415,7 @@ export function FeedsScreen(_props: Props) { </View> ) } else if (item.type === 'savedFeedsHeader') { - return ( - <> - {!isMobile && ( - <View - style={[ - pal.view, - styles.header, - pal.border, - { - borderBottomWidth: 1, - }, - ]}> - <Text type="title-lg" style={[pal.text, s.bold]}> - <Trans>Feeds</Trans> - </Text> - <View style={styles.headerBtnGroup}> - <TextLink - type="lg" - href="/settings/saved-feeds" - accessibilityLabel={_(msg`Edit My Feeds`)} - accessibilityHint="" - text={_(msg`Edit`)} - style={[pal.link]} - /> - </View> - </View> - )} - <FeedsSavedHeader /> - </> - ) + return <FeedsSavedHeader /> } else if (item.type === 'savedFeedNoResults') { return ( <View @@ -530,13 +486,8 @@ export function FeedsScreen(_props: Props) { return null }, [ - isMobile, - pal.view, pal.border, - pal.text, pal.textLight, - pal.link, - _, query, onChangeQuery, onPressCancelSearch, @@ -547,31 +498,45 @@ export function FeedsScreen(_props: Props) { return ( <Layout.Screen testID="FeedsScreen"> - {isMobile && ( - <ViewHeader - title={_(msg`Feeds`)} - renderButton={renderHeaderBtn} - showBorder + <Layout.Center> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Feeds</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot> + <Link + testID="editFeedsBtn" + to="/settings/saved-feeds" + label={_(msg`Edit My Feeds`)} + size="small" + variant="ghost" + color="secondary" + shape="round" + style={[a.justify_center, {right: -3}]}> + <ButtonIcon icon={Gear} size="lg" /> + </Link> + </Layout.Header.Slot> + </Layout.Header.Outer> + + <List + ref={listRef} + data={items} + keyExtractor={item => item.key} + contentContainerStyle={styles.contentContainer} + renderItem={renderItem} + refreshing={isPTR} + onRefresh={isUserSearching ? undefined : onPullToRefresh} + initialNumToRender={10} + onEndReached={onEndReached} + desktopFixedHeight + keyboardShouldPersistTaps="handled" + keyboardDismissMode="on-drag" + sideBorders={false} /> - )} - - <List - ref={listRef} - style={[!isTabletOrDesktop && s.flex1, styles.list]} - data={items} - keyExtractor={item => item.key} - contentContainerStyle={styles.contentContainer} - renderItem={renderItem} - refreshing={isPTR} - onRefresh={isUserSearching ? undefined : onPullToRefresh} - initialNumToRender={10} - onEndReached={onEndReached} - // @ts-ignore our .web version only -prf - desktopFixedHeight - scrollIndicatorInsets={{right: 1}} - keyboardShouldPersistTaps="handled" - keyboardDismissMode="on-drag" - /> + </Layout.Center> {hasSession && ( <FAB @@ -728,7 +693,7 @@ function FeedsSavedHeader() { }> <IconCircle icon={ListSparkle_Stroke2_Corner0_Rounded} size="lg" /> <View style={[a.flex_1, a.gap_xs]}> - <Text style={[a.flex_1, a.text_2xl, a.font_bold, t.atoms.text]}> + <Text style={[a.flex_1, a.text_2xl, a.font_heavy, t.atoms.text]}> <Trans>My Feeds</Trans> </Text> <Text style={[t.atoms.text_contrast_high]}> @@ -754,7 +719,7 @@ function FeedsAboutHeader() { size="lg" /> <View style={[a.flex_1, a.gap_sm]}> - <Text style={[a.flex_1, a.text_2xl, a.font_bold, t.atoms.text]}> + <Text style={[a.flex_1, a.text_2xl, a.font_heavy, t.atoms.text]}> <Trans>Discover New Feeds</Trans> </Text> <Text style={[t.atoms.text_contrast_high]}> @@ -769,9 +734,6 @@ function FeedsAboutHeader() { } const styles = StyleSheet.create({ - list: { - height: '100%', - }, contentContainer: { paddingBottom: 100, }, diff --git a/src/view/screens/Lists.tsx b/src/view/screens/Lists.tsx index f654f2bd9..99abf0603 100644 --- a/src/view/screens/Lists.tsx +++ b/src/view/screens/Lists.tsx @@ -1,33 +1,26 @@ import React from 'react' -import {StyleSheet, View} from 'react-native' import {AtUri} from '@atproto/api' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect, useNavigation} from '@react-navigation/native' import {useEmail} from '#/lib/hooks/useEmail' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {NavigationProp} from '#/lib/routes/types' -import {s} from '#/lib/styles' import {useModalControls} from '#/state/modals' import {useSetMinimalShellMode} from '#/state/shell' import {MyLists} from '#/view/com/lists/MyLists' -import {Button} from '#/view/com/util/forms/Button' -import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' -import {Text} from '#/view/com/util/text/Text' +import {atoms as a} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' +import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' import * as Layout from '#/components/Layout' type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'> export function ListsScreen({}: Props) { const {_} = useLingui() - const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode() - const {isMobile} = useWebMediaQueries() const navigation = useNavigation<NavigationProp>() const {openModal} = useModalControls() const {needsEmailVerification} = useEmail() @@ -62,43 +55,30 @@ export function ListsScreen({}: Props) { return ( <Layout.Screen testID="listsScreen"> - <SimpleViewHeader - showBackButton={isMobile} - style={[ - pal.border, - isMobile - ? {borderBottomWidth: StyleSheet.hairlineWidth} - : { - borderLeftWidth: StyleSheet.hairlineWidth, - borderRightWidth: StyleSheet.hairlineWidth, - }, - ]}> - <View style={{flex: 1}}> - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> - <Trans>User Lists</Trans> - </Text> - <Text style={pal.textLight}> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content align="left"> + <Layout.Header.TitleText> + <Trans>Lists</Trans> + </Layout.Header.TitleText> + <Layout.Header.SubtitleText> <Trans>Public, shareable lists which can drive feeds.</Trans> - </Text> - </View> - <View style={[{marginLeft: 18}, isMobile && {marginLeft: 12}]}> - <Button - testID="newUserListBtn" - type="default" - onPress={onPressNewList} - style={{ - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }}> - <FontAwesomeIcon icon="plus" color={pal.colors.text} /> - <Text type="button" style={pal.text}> - <Trans context="action">New</Trans> - </Text> - </Button> - </View> - </SimpleViewHeader> - <MyLists filter="curate" style={s.flexGrow1} /> + </Layout.Header.SubtitleText> + </Layout.Header.Content> + <Button + label={_(msg`New list`)} + testID="newUserListBtn" + color="secondary" + variant="solid" + size="small" + onPress={onPressNewList}> + <ButtonIcon icon={PlusIcon} /> + <ButtonText> + <Trans context="action">New</Trans> + </ButtonText> + </Button> + </Layout.Header.Outer> + <MyLists filter="curate" style={a.flex_grow} /> <VerifyEmailDialog reasonText={_( msg`Before creating a list, you must first verify your email.`, diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx index 53e31d1d2..9b0f54984 100644 --- a/src/view/screens/ModerationBlockedAccounts.tsx +++ b/src/view/screens/ModerationBlockedAccounts.tsx @@ -23,7 +23,6 @@ import {ProfileCard} from '#/view/com/profile/ProfileCard' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {Text} from '#/view/com/util/text/Text' import {ViewHeader} from '#/view/com/util/ViewHeader' -import {CenteredView} from '#/view/com/util/Views' import * as Layout from '#/components/Layout' type Props = NativeStackScreenProps< @@ -97,14 +96,7 @@ export function ModerationBlockedAccounts({}: Props) { ) return ( <Layout.Screen testID="blockedAccountsScreen"> - <CenteredView - style={[ - styles.container, - isTabletOrDesktop && styles.containerDesktop, - pal.view, - pal.border, - ]} - testID="blockedAccountsScreen"> + <Layout.Center> <ViewHeader title={_(msg`Blocked Accounts`)} showOnDesktop /> <Text type="sm" @@ -112,6 +104,9 @@ export function ModerationBlockedAccounts({}: Props) { styles.description, pal.text, isTabletOrDesktop && styles.descriptionDesktop, + { + marginTop: 20, + }, ]}> <Trans> Blocked accounts cannot reply in your threads, mention you, or @@ -120,7 +115,7 @@ export function ModerationBlockedAccounts({}: Props) { </Trans> </Text> {isEmpty ? ( - <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}> + <View style={[pal.border]}> {isError ? ( <ErrorScreen title="Oops!" @@ -166,21 +161,12 @@ export function ModerationBlockedAccounts({}: Props) { desktopFixedHeight /> )} - </CenteredView> + </Layout.Center> </Layout.Screen> ) } const styles = StyleSheet.create({ - container: { - flex: 1, - paddingBottom: 100, - }, - containerDesktop: { - borderLeftWidth: 1, - borderRightWidth: 1, - paddingBottom: 0, - }, title: { textAlign: 'center', marginTop: 12, diff --git a/src/view/screens/ModerationModlists.tsx b/src/view/screens/ModerationModlists.tsx index c623c5376..0ef4d4389 100644 --- a/src/view/screens/ModerationModlists.tsx +++ b/src/view/screens/ModerationModlists.tsx @@ -1,33 +1,26 @@ import React from 'react' -import {View} from 'react-native' import {AtUri} from '@atproto/api' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect, useNavigation} from '@react-navigation/native' import {useEmail} from '#/lib/hooks/useEmail' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {NavigationProp} from '#/lib/routes/types' -import {s} from '#/lib/styles' import {useModalControls} from '#/state/modals' import {useSetMinimalShellMode} from '#/state/shell' import {MyLists} from '#/view/com/lists/MyLists' -import {Button} from '#/view/com/util/forms/Button' -import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' -import {Text} from '#/view/com/util/text/Text' +import {atoms as a} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' +import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' import * as Layout from '#/components/Layout' type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'> export function ModerationModlistsScreen({}: Props) { const {_} = useLingui() - const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode() - const {isMobile} = useWebMediaQueries() const navigation = useNavigation<NavigationProp>() const {openModal} = useModalControls() const {needsEmailVerification} = useEmail() @@ -62,39 +55,32 @@ export function ModerationModlistsScreen({}: Props) { return ( <Layout.Screen testID="moderationModlistsScreen"> - <SimpleViewHeader - showBackButton={isMobile} - style={ - !isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}] - }> - <View style={{flex: 1}}> - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content align="left"> + <Layout.Header.TitleText> <Trans>Moderation Lists</Trans> - </Text> - <Text style={pal.textLight}> + </Layout.Header.TitleText> + <Layout.Header.SubtitleText> <Trans> Public, shareable lists of users to mute or block in bulk. </Trans> - </Text> - </View> - <View style={[{marginLeft: 18}, isMobile && {marginLeft: 12}]}> - <Button - testID="newModListBtn" - type="default" - onPress={onPressNewList} - style={{ - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }}> - <FontAwesomeIcon icon="plus" color={pal.colors.text} /> - <Text type="button" style={pal.text}> - <Trans>New</Trans> - </Text> - </Button> - </View> - </SimpleViewHeader> - <MyLists filter="mod" style={s.flexGrow1} /> + </Layout.Header.SubtitleText> + </Layout.Header.Content> + <Button + label={_(msg`New list`)} + testID="newModListBtn" + color="secondary" + variant="solid" + size="small" + onPress={onPressNewList}> + <ButtonIcon icon={PlusIcon} /> + <ButtonText> + <Trans context="action">New</Trans> + </ButtonText> + </Button> + </Layout.Header.Outer> + <MyLists filter="mod" style={a.flex_grow} /> <VerifyEmailDialog reasonText={_( msg`Before creating a list, you must first verify your email.`, diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx index 6d34c8a5f..6a8c6c3e6 100644 --- a/src/view/screens/ModerationMutedAccounts.tsx +++ b/src/view/screens/ModerationMutedAccounts.tsx @@ -23,7 +23,6 @@ import {ProfileCard} from '#/view/com/profile/ProfileCard' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {Text} from '#/view/com/util/text/Text' import {ViewHeader} from '#/view/com/util/ViewHeader' -import {CenteredView} from '#/view/com/util/Views' import * as Layout from '#/components/Layout' type Props = NativeStackScreenProps< @@ -97,21 +96,17 @@ export function ModerationMutedAccounts({}: Props) { ) return ( <Layout.Screen testID="mutedAccountsScreen"> - <CenteredView - style={[ - styles.container, - isTabletOrDesktop && styles.containerDesktop, - pal.view, - pal.border, - ]} - testID="mutedAccountsScreen"> - <ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop /> + <ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop /> + <Layout.Center> <Text type="sm" style={[ styles.description, pal.text, isTabletOrDesktop && styles.descriptionDesktop, + { + marginTop: 20, + }, ]}> <Trans> Muted accounts have their posts removed from your feed and from your @@ -119,7 +114,7 @@ export function ModerationMutedAccounts({}: Props) { </Trans> </Text> {isEmpty ? ( - <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}> + <View style={[pal.border]}> {isError ? ( <ErrorScreen title="Oops!" @@ -165,21 +160,12 @@ export function ModerationMutedAccounts({}: Props) { desktopFixedHeight /> )} - </CenteredView> + </Layout.Center> </Layout.Screen> ) } const styles = StyleSheet.create({ - container: { - flex: 1, - paddingBottom: 100, - }, - containerDesktop: { - borderLeftWidth: 1, - borderRightWidth: 1, - paddingBottom: 0, - }, title: { textAlign: 'center', marginTop: 12, diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index 531d10a7f..35591f270 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react' +import React from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -6,7 +6,6 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {ComposeIcon2} from '#/lib/icons' import { NativeStackScreenProps, @@ -14,7 +13,7 @@ import { } from '#/lib/routes/types' import {s} from '#/lib/styles' import {logger} from '#/logger' -import {isNative} from '#/platform/detection' +import {isNative, isWeb} from '#/platform/detection' import {emitSoftReset, listenSoftReset} from '#/state/events' import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed' import { @@ -29,28 +28,25 @@ import {FAB} from '#/view/com/util/fab/FAB' import {ListMethods} from '#/view/com/util/List' import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' import {MainScrollProvider} from '#/view/com/util/MainScrollProvider' -import {ViewHeader} from '#/view/com/util/ViewHeader' -import {CenteredView} from '#/view/com/util/Views' -import {atoms as a, useTheme} from '#/alf' -import {Button} from '#/components/Button' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Button, ButtonIcon} from '#/components/Button' import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2' import * as Layout from '#/components/Layout' import {Link} from '#/components/Link' import {Loader} from '#/components/Loader' -import {Text} from '#/components/Typography' type Props = NativeStackScreenProps< NotificationsTabNavigatorParams, 'Notifications' > export function NotificationsScreen({route: {params}}: Props) { + const t = useTheme() + const {gtTablet} = useBreakpoints() const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode() const [isScrolledDown, setIsScrolledDown] = React.useState(false) const [isLoadingLatest, setIsLoadingLatest] = React.useState(false) const scrollElRef = React.useRef<ListMethods>(null) - const t = useTheme() - const {isDesktop} = useWebMediaQueries() const queryClient = useQueryClient() const unreadNotifs = useUnreadNotifications() const unreadApi = useUnreadNotificationsApi() @@ -110,121 +106,77 @@ export function NotificationsScreen({route: {params}}: Props) { return listenSoftReset(onPressLoadLatest) }, [onPressLoadLatest, isScreenFocused]) - const renderButton = useCallback(() => { - return ( - <Link - to="/notifications/settings" - label={_(msg`Notification settings`)} - size="small" - variant="ghost" - color="secondary" - shape="square" - style={[a.justify_center]}> - <SettingsIcon size="md" style={t.atoms.text_contrast_medium} /> - </Link> - ) - }, [_, t]) - - const ListHeaderComponent = React.useCallback(() => { - if (isDesktop) { - return ( - <View - style={[ - t.atoms.bg, - a.flex_row, - a.align_center, - a.justify_between, - a.gap_lg, - a.px_lg, - a.pr_md, - a.py_sm, - ]}> + return ( + <Layout.Screen testID="notificationsScreen"> + <Layout.Header.Outer> + <Layout.Header.MenuButton /> + <Layout.Header.Content> <Button label={_(msg`Notifications`)} accessibilityHint={_(msg`Refresh notifications`)} - onPress={emitSoftReset}> - {({hovered, pressed}) => ( - <Text - style={[ - a.text_2xl, - a.font_bold, - (hovered || pressed) && a.underline, - ]}> + onPress={emitSoftReset} + style={[a.justify_start]}> + {({hovered}) => ( + <Layout.Header.TitleText + style={[a.w_full, hovered && a.underline]}> <Trans>Notifications</Trans> - {hasNew && ( + {isWeb && gtTablet && hasNew && ( <View - style={{ - left: 4, - top: -8, - backgroundColor: t.palette.primary_500, - width: 8, - height: 8, - borderRadius: 4, - }} + style={[ + a.rounded_full, + { + width: 8, + height: 8, + bottom: 3, + left: 6, + backgroundColor: t.palette.primary_500, + }, + ]} /> )} - </Text> + </Layout.Header.TitleText> )} </Button> - <View style={[a.flex_row, a.align_center, a.gap_sm]}> - {isLoadingLatest ? <Loader size="md" /> : <></>} - {renderButton()} - </View> - </View> - ) - } - return <></> - }, [isDesktop, t, hasNew, renderButton, _, isLoadingLatest]) - - const renderHeaderSpinner = React.useCallback(() => { - return ( - <View - style={[ - {width: 30, height: 20}, - a.flex_row, - a.align_center, - a.justify_end, - a.gap_md, - ]}> - {isLoadingLatest ? <Loader width={20} /> : <></>} - {renderButton()} - </View> - ) - }, [renderButton, isLoadingLatest]) + </Layout.Header.Content> + <Layout.Header.Slot> + <Link + to="/notifications/settings" + label={_(msg`Notification settings`)} + size="small" + variant="ghost" + color="secondary" + shape="round" + style={[a.justify_center]}> + <ButtonIcon + icon={isLoadingLatest ? Loader : SettingsIcon} + size="lg" + /> + </Link> + </Layout.Header.Slot> + </Layout.Header.Outer> - return ( - <Layout.Screen testID="notificationsScreen"> - <CenteredView style={[a.flex_1, {paddingTop: 2}]} sideBorders={true}> - <ViewHeader - title={_(msg`Notifications`)} - canGoBack={false} - showBorder={true} - renderButton={renderHeaderSpinner} + <MainScrollProvider> + <Feed + onScrolledDownChange={setIsScrolledDown} + scrollElRef={scrollElRef} + overridePriorityNotifications={params?.show === 'all'} /> - <MainScrollProvider> - <Feed - onScrolledDownChange={setIsScrolledDown} - scrollElRef={scrollElRef} - ListHeaderComponent={ListHeaderComponent} - overridePriorityNotifications={params?.show === 'all'} - /> - </MainScrollProvider> - {(isScrolledDown || hasNew) && ( - <LoadLatestBtn - onPress={onPressLoadLatest} - label={_(msg`Load new notifications`)} - showIndicator={hasNew} - /> - )} - <FAB - testID="composeFAB" - onPress={() => openComposer({})} - icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} - accessibilityRole="button" - accessibilityLabel={_(msg`New post`)} - accessibilityHint="" + </MainScrollProvider> + {(isScrolledDown || hasNew) && ( + <LoadLatestBtn + onPress={onPressLoadLatest} + label={_(msg`Load new notifications`)} + showIndicator={hasNew} /> - </CenteredView> + )} + <FAB + testID="composeFAB" + onPress={() => openComposer({})} + icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} + accessibilityRole="button" + accessibilityLabel={_(msg`New post`)} + accessibilityHint="" + /> </Layout.Screen> ) } diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index c183569b7..1bad9b6cd 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -1,12 +1,10 @@ import React from 'react' -import {View} from 'react-native' import {useFocusEffect} from '@react-navigation/native' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {makeRecordUri} from '#/lib/strings/url-helpers' import {useSetMinimalShellMode} from '#/state/shell' import {PostThread as PostThreadComponent} from '#/view/com/post-thread/PostThread' -import {atoms as a} from '#/alf' import * as Layout from '#/components/Layout' type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'> @@ -24,9 +22,7 @@ export function PostThreadScreen({route}: Props) { return ( <Layout.Screen testID="postThreadScreen"> - <View style={a.flex_1}> - <PostThreadComponent uri={uri} /> - </View> + <PostThreadComponent uri={uri} /> </Layout.Screen> ) } diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 677fe09f4..6a9b6f7f2 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -40,11 +40,9 @@ import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {FAB} from '#/view/com/util/fab/FAB' import {ListRef} from '#/view/com/util/List' -import {CenteredView} from '#/view/com/util/Views' import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header' import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed' import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels' -import {web} from '#/alf' import * as Layout from '#/components/Layout' import {ScreenHider} from '#/components/moderation/ScreenHider' import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks' @@ -116,9 +114,9 @@ function ProfileScreenInner({route}: Props) { // Most pushes will happen here, since we will have only placeholder data if (isLoadingDid || isLoadingProfile || starterPacksQuery.isLoading) { return ( - <CenteredView sideBorders style={web({height: '100vh'})}> + <Layout.Content> <ProfileHeaderLoading /> - </CenteredView> + </Layout.Content> ) } if (resolveError || profileError) { diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index b34f0f1b0..63469ef4f 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -49,7 +49,6 @@ import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' import {LoadingScreen} from '#/view/com/util/LoadingScreen' import {Text} from '#/view/com/util/text/Text' import * as Toast from '#/view/com/util/Toast' -import {CenteredView} from '#/view/com/util/Views' import {atoms as a, useTheme} from '#/alf' import {Button as NewButton, ButtonText} from '#/components/Button' import {useRichText} from '#/components/hooks/useRichText' @@ -98,7 +97,7 @@ export function ProfileFeedScreen(props: Props) { if (error) { return ( <Layout.Screen testID="profileFeedScreenError"> - <CenteredView> + <Layout.Content> <View style={[pal.view, pal.border, styles.notFoundContainer]}> <Text type="title-lg" style={[pal.text, s.mb10]}> <Trans>Could not load feed</Trans> @@ -120,7 +119,7 @@ export function ProfileFeedScreen(props: Props) { </Button> </View> </View> - </CenteredView> + </Layout.Content> </Layout.Screen> ) } @@ -394,7 +393,7 @@ export function ProfileFeedScreenInner({ ]) return ( - <View style={s.hContentRegion}> + <> <ReportDialog control={reportDialogControl} params={{ @@ -434,7 +433,7 @@ export function ProfileFeedScreenInner({ accessibilityHint="" /> )} - </View> + </> ) } diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx index 9fa98cb1a..90c0a57f9 100644 --- a/src/view/screens/ProfileFollowers.tsx +++ b/src/view/screens/ProfileFollowers.tsx @@ -10,7 +10,6 @@ import {ProfileFollowers as ProfileFollowersComponent} from '#/view/com/profile/ import {ViewHeader} from '#/view/com/util/ViewHeader' import {CenteredView} from '#/view/com/util/Views' import * as Layout from '#/components/Layout' -import {ListHeaderDesktop} from '#/components/Lists' type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'> export const ProfileFollowersScreen = ({route}: Props) => { @@ -27,7 +26,6 @@ export const ProfileFollowersScreen = ({route}: Props) => { return ( <Layout.Screen testID="profileFollowersScreen"> <CenteredView sideBorders={true}> - <ListHeaderDesktop title={_(msg`Followers`)} /> <ViewHeader title={_(msg`Followers`)} showBorder={!isWeb} /> <ProfileFollowersComponent name={name} /> </CenteredView> diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx index 483ee93ec..134f79993 100644 --- a/src/view/screens/ProfileFollows.tsx +++ b/src/view/screens/ProfileFollows.tsx @@ -10,7 +10,6 @@ import {ProfileFollows as ProfileFollowsComponent} from '#/view/com/profile/Prof import {ViewHeader} from '#/view/com/util/ViewHeader' import {CenteredView} from '#/view/com/util/Views' import * as Layout from '#/components/Layout' -import {ListHeaderDesktop} from '#/components/Lists' type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'> export const ProfileFollowsScreen = ({route}: Props) => { @@ -27,7 +26,6 @@ export const ProfileFollowsScreen = ({route}: Props) => { return ( <Layout.Screen testID="profileFollowsScreen"> <CenteredView sideBorders={true}> - <ListHeaderDesktop title={_(msg`Following`)} /> <ViewHeader title={_(msg`Following`)} showBorder={!isWeb} /> <ProfileFollowsComponent name={name} /> </CenteredView> diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index cb333befa..a927526ad 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -69,7 +69,6 @@ import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' import {LoadingScreen} from '#/view/com/util/LoadingScreen' import {Text} from '#/view/com/util/text/Text' import * as Toast from '#/view/com/util/Toast' -import {CenteredView} from '#/view/com/util/Views' import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen' import {atoms as a, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' @@ -107,20 +106,20 @@ function ProfileListScreenInner(props: Props) { if (resolveError) { return ( - <CenteredView> + <Layout.Content> <ErrorScreen error={_( msg`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`, )} /> - </CenteredView> + </Layout.Content> ) } if (listError) { return ( - <CenteredView> + <Layout.Content> <ErrorScreen error={cleanError(listError)} /> - </CenteredView> + </Layout.Content> ) } @@ -1010,7 +1009,6 @@ function ErrorScreen({error}: {error: string}) { pal.view, pal.border, { - marginTop: 10, paddingHorizontal: 18, paddingVertical: 14, borderTopWidth: StyleSheet.hairlineWidth, diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 3c04ec36f..1b4c84a60 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -25,13 +25,12 @@ import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard' import {TextLink} from '#/view/com/util/Link' import {Text} from '#/view/com/util/text/Text' import * as Toast from '#/view/com/util/Toast' -import {ViewHeader} from '#/view/com/util/ViewHeader' -import {CenteredView, ScrollView} from '#/view/com/util/Views' import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed' import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' +import {FloppyDisk_Stroke2_Corner0_Rounded as SaveIcon} from '#/components/icons/FloppyDisk' import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' @@ -51,7 +50,7 @@ function SavedFeedsInner({ }) { const pal = usePalette('default') const {_} = useLingui() - const {isMobile, isTabletOrDesktop, isDesktop} = useWebMediaQueries() + const {isMobile, isDesktop} = useWebMediaQueries() const setMinimalShellMode = useSetMinimalShellMode() const {mutateAsync: overwriteSavedFeeds, isPending: isOverwritePending} = useOverwriteSavedFeedsMutation() @@ -88,136 +87,128 @@ function SavedFeedsInner({ } }, [_, overwriteSavedFeeds, currentFeeds, navigation]) - const renderHeaderBtn = React.useCallback(() => { - return ( - <Button - size="small" - variant={hasUnsavedChanges ? 'solid' : 'solid'} - color={hasUnsavedChanges ? 'primary' : 'secondary'} - onPress={onSaveChanges} - label={_(msg`Save changes`)} - disabled={isOverwritePending || !hasUnsavedChanges} - style={[isDesktop && a.mt_sm]} - testID="saveChangesBtn"> - <ButtonText style={[isDesktop && a.text_md]}> - {isDesktop ? <Trans>Save changes</Trans> : <Trans>Save</Trans>} - </ButtonText> - {isOverwritePending && <ButtonIcon icon={Loader} />} - </Button> - ) - }, [_, isDesktop, onSaveChanges, hasUnsavedChanges, isOverwritePending]) - return ( <Layout.Screen> - <CenteredView - style={[a.util_screen_outer]} - sideBorders={isTabletOrDesktop}> - <ViewHeader - title={_(msg`Edit My Feeds`)} - showOnDesktop - showBorder - renderButton={renderHeaderBtn} - /> - <ScrollView style={[a.flex_1]} contentContainerStyle={[a.border_0]}> - {noSavedFeedsOfAnyType && ( - <View style={[pal.border, a.border_b]}> - <NoSavedFeedsOfAnyType /> - </View> - )} + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content align="left"> + <Layout.Header.TitleText> + <Trans>Feeds</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Button + testID="saveChangesBtn" + size="small" + variant={hasUnsavedChanges ? 'solid' : 'solid'} + color={hasUnsavedChanges ? 'primary' : 'secondary'} + onPress={onSaveChanges} + label={_(msg`Save changes`)} + disabled={isOverwritePending || !hasUnsavedChanges}> + <ButtonIcon icon={isOverwritePending ? Loader : SaveIcon} /> + <ButtonText> + {isDesktop ? <Trans>Save changes</Trans> : <Trans>Save</Trans>} + </ButtonText> + </Button> + </Layout.Header.Outer> - <View style={[pal.text, pal.border, styles.title]}> - <Text type="title" style={pal.text}> - <Trans>Pinned Feeds</Trans> - </Text> + <Layout.Content> + {noSavedFeedsOfAnyType && ( + <View style={[pal.border, a.border_b]}> + <NoSavedFeedsOfAnyType /> </View> + )} - {preferences ? ( - !pinnedFeeds.length ? ( - <View - style={[ - pal.border, - isMobile && s.flex1, - pal.viewLight, - styles.empty, - ]}> - <Text type="lg" style={[pal.text]}> - <Trans>You don't have any pinned feeds.</Trans> - </Text> - </View> - ) : ( - pinnedFeeds.map(f => ( - <ListItem - key={f.id} - feed={f} - isPinned - currentFeeds={currentFeeds} - setCurrentFeeds={setCurrentFeeds} - preferences={preferences} - /> - )) - ) - ) : ( - <ActivityIndicator style={{marginTop: 20}} /> - )} + <View style={[pal.text, pal.border, styles.title]}> + <Text type="title" style={pal.text}> + <Trans>Pinned Feeds</Trans> + </Text> + </View> - {noFollowingFeed && ( - <View style={[pal.border, a.border_b]}> - <NoFollowingFeed /> + {preferences ? ( + !pinnedFeeds.length ? ( + <View + style={[ + pal.border, + isMobile && s.flex1, + pal.viewLight, + styles.empty, + ]}> + <Text type="lg" style={[pal.text]}> + <Trans>You don't have any pinned feeds.</Trans> + </Text> </View> - )} + ) : ( + pinnedFeeds.map(f => ( + <ListItem + key={f.id} + feed={f} + isPinned + currentFeeds={currentFeeds} + setCurrentFeeds={setCurrentFeeds} + preferences={preferences} + /> + )) + ) + ) : ( + <ActivityIndicator style={{marginTop: 20}} /> + )} - <View style={[pal.text, pal.border, styles.title]}> - <Text type="title" style={pal.text}> - <Trans>Saved Feeds</Trans> - </Text> + {noFollowingFeed && ( + <View style={[pal.border, a.border_b]}> + <NoFollowingFeed /> </View> - {preferences ? ( - !unpinnedFeeds.length ? ( - <View - style={[ - pal.border, - isMobile && s.flex1, - pal.viewLight, - styles.empty, - ]}> - <Text type="lg" style={[pal.text]}> - <Trans>You don't have any saved feeds.</Trans> - </Text> - </View> - ) : ( - unpinnedFeeds.map(f => ( - <ListItem - key={f.id} - feed={f} - isPinned={false} - currentFeeds={currentFeeds} - setCurrentFeeds={setCurrentFeeds} - preferences={preferences} - /> - )) - ) + )} + + <View style={[pal.text, pal.border, styles.title]}> + <Text type="title" style={pal.text}> + <Trans>Saved Feeds</Trans> + </Text> + </View> + {preferences ? ( + !unpinnedFeeds.length ? ( + <View + style={[ + pal.border, + isMobile && s.flex1, + pal.viewLight, + styles.empty, + ]}> + <Text type="lg" style={[pal.text]}> + <Trans>You don't have any saved feeds.</Trans> + </Text> + </View> ) : ( - <ActivityIndicator style={{marginTop: 20}} /> - )} + unpinnedFeeds.map(f => ( + <ListItem + key={f.id} + feed={f} + isPinned={false} + currentFeeds={currentFeeds} + setCurrentFeeds={setCurrentFeeds} + preferences={preferences} + /> + )) + ) + ) : ( + <ActivityIndicator style={{marginTop: 20}} /> + )} - <View style={styles.footerText}> - <Text type="sm" style={pal.textLight}> - <Trans> - Feeds are custom algorithms that users build with a little - coding expertise.{' '} - <TextLink - type="sm" - style={pal.link} - href="https://github.com/bluesky-social/feed-generator" - text={_(msg`See this guide`)} - />{' '} - for more information. - </Trans> - </Text> - </View> - <View style={{height: 100}} /> - </ScrollView> - </CenteredView> + <View style={styles.footerText}> + <Text type="sm" style={pal.textLight}> + <Trans> + Feeds are custom algorithms that users build with a little coding + expertise.{' '} + <TextLink + type="sm" + style={pal.link} + href="https://github.com/bluesky-social/feed-generator" + text={_(msg`See this guide`)} + />{' '} + for more information. + </Trans> + </Text> + </View> + </Layout.Content> </Layout.Screen> ) } @@ -456,7 +447,6 @@ const styles = StyleSheet.create({ }, footerText: { paddingHorizontal: 26, - paddingTop: 22, - paddingBottom: 100, + paddingVertical: 22, }, }) diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index 0518bc506..0871458c9 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -55,7 +55,6 @@ import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' import {Link} from '#/view/com/util/Link' import {List} from '#/view/com/util/List' import {Text} from '#/view/com/util/text/Text' -import {CenteredView, ScrollView} from '#/view/com/util/Views' import {Explore} from '#/view/screens/Search/Explore' import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search' import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils' @@ -68,63 +67,46 @@ import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' import * as Layout from '#/components/Layout' function Loader() { - const pal = usePalette('default') - const {isMobile} = useWebMediaQueries() return ( - <CenteredView - style={[ - // @ts-ignore web only -prf - { - padding: 18, - height: isWeb ? '100vh' : undefined, - }, - pal.border, - ]} - sideBorders={!isMobile}> - <ActivityIndicator /> - </CenteredView> + <Layout.Content> + <View style={[a.py_xl]}> + <ActivityIndicator /> + </View> + </Layout.Content> ) } function EmptyState({message, error}: {message: string; error?: string}) { const pal = usePalette('default') - const {isMobile} = useWebMediaQueries() return ( - <CenteredView - sideBorders={!isMobile} - style={[ - pal.border, - // @ts-ignore web only -prf - { - padding: 18, - height: isWeb ? '100vh' : undefined, - }, - ]}> - <View style={[pal.viewLight, {padding: 18, borderRadius: 8}]}> - <Text style={[pal.text]}>{message}</Text> + <Layout.Content> + <View style={[a.p_xl]}> + <View style={[pal.viewLight, {padding: 18, borderRadius: 8}]}> + <Text style={[pal.text]}>{message}</Text> - {error && ( - <> - <View - style={[ - { - marginVertical: 12, - height: 1, - width: '100%', - backgroundColor: pal.text.color, - opacity: 0.2, - }, - ]} - /> + {error && ( + <> + <View + style={[ + { + marginVertical: 12, + height: 1, + width: '100%', + backgroundColor: pal.text.color, + opacity: 0.2, + }, + ]} + /> - <Text style={[pal.textLight]}> - <Trans>Error:</Trans> {error} - </Text> - </> - )} + <Text style={[pal.textLight]}> + <Trans>Error:</Trans> {error} + </Text> + </> + )} + </View> </View> - </CenteredView> + </Layout.Content> ) } @@ -224,7 +206,7 @@ let SearchScreenPostResults = ({ if (item.type === 'post') { return <Post post={item.post} /> } else { - return <Loader /> + return null } }} keyExtractor={item => item.key} @@ -550,19 +532,13 @@ let SearchScreenInner = ({ <Pager onPageSelected={onPageSelected} renderTabBar={props => ( - <CenteredView - sideBorders + <Layout.Center style={[ - pal.border, - pal.view, - web({ - position: isWeb ? 'sticky' : '', - zIndex: 1, - }), + web([a.sticky, a.z_10]), {top: isWeb ? headerHeight : undefined}, ]}> <TabBar items={sections.map(section => section.title)} {...props} /> - </CenteredView> + </Layout.Center> )} initialPage={0}> {sections.map((section, i) => ( @@ -572,7 +548,7 @@ let SearchScreenInner = ({ ) : hasSession ? ( <Explore /> ) : ( - <CenteredView sideBorders style={pal.border}> + <Layout.Center> <View // @ts-ignore web only -esb style={{ @@ -614,7 +590,7 @@ let SearchScreenInner = ({ </Text> </View> </View> - </CenteredView> + </Layout.Center> ) } SearchScreenInner = React.memo(SearchScreenInner) @@ -650,7 +626,7 @@ export function SearchScreen( * Arbitrary sizing, so guess and check, used for sticky header alignment and * sizing. */ - const headerHeight = 64 + (showFilters ? 40 : 0) + const headerHeight = 60 + (showFilters ? 40 : 0) useFocusEffect( useNonReactiveCallback(() => { @@ -861,73 +837,79 @@ export function SearchScreen( return ( <Layout.Screen testID="searchScreen"> - <CenteredView + <View style={[ - a.p_md, - a.pb_sm, - a.gap_sm, - t.atoms.bg, web({ height: headerHeight, position: 'sticky', top: 0, zIndex: 1, }), - ]} - sideBorders={gtMobile}> - <View style={[a.flex_row, a.gap_sm]}> - {!gtMobile && ( - <Button - testID="viewHeaderBackOrMenuBtn" - onPress={onPressMenu} - hitSlop={HITSLOP_10} - label={_(msg`Menu`)} - accessibilityHint={_(msg`Access navigation links and settings`)} - size="large" - variant="solid" - color="secondary" - shape="square"> - <ButtonIcon icon={Menu} size="lg" /> - </Button> - )} - <View style={[a.flex_1]}> - <SearchInput - ref={textInput} - value={searchText} - onFocus={onSearchInputFocus} - onChangeText={onChangeText} - onClearText={onPressClearQuery} - onSubmitEditing={onSubmit} - /> - </View> - {showAutocomplete && ( - <Button - label={_(msg`Cancel search`)} - size="large" - variant="ghost" - color="secondary" - style={[a.px_sm]} - onPress={onPressCancelSearch} - hitSlop={HITSLOP_10}> - <ButtonText> - <Trans>Cancel</Trans> - </ButtonText> - </Button> - )} - </View> - - {showFilters && ( - <View - style={[a.flex_row, a.align_center, a.justify_between, a.gap_sm]}> - <View style={[{width: 140}]}> - <SearchLanguageDropdown - value={params.lang} - onChange={params.setLang} - /> + ]}> + <Layout.Center> + <View style={[a.p_md, a.pb_sm, a.gap_sm, t.atoms.bg]}> + <View style={[a.flex_row, a.gap_sm]}> + {!gtMobile && ( + <Button + testID="viewHeaderBackOrMenuBtn" + onPress={onPressMenu} + hitSlop={HITSLOP_10} + label={_(msg`Menu`)} + accessibilityHint={_( + msg`Access navigation links and settings`, + )} + size="large" + variant="solid" + color="secondary" + shape="square"> + <ButtonIcon icon={Menu} size="lg" /> + </Button> + )} + <View style={[a.flex_1]}> + <SearchInput + ref={textInput} + value={searchText} + onFocus={onSearchInputFocus} + onChangeText={onChangeText} + onClearText={onPressClearQuery} + onSubmitEditing={onSubmit} + /> + </View> + {showAutocomplete && ( + <Button + label={_(msg`Cancel search`)} + size="large" + variant="ghost" + color="secondary" + style={[a.px_sm]} + onPress={onPressCancelSearch} + hitSlop={HITSLOP_10}> + <ButtonText> + <Trans>Cancel</Trans> + </ButtonText> + </Button> + )} </View> + + {showFilters && ( + <View + style={[ + a.flex_row, + a.align_center, + a.justify_between, + a.gap_sm, + ]}> + <View style={[{width: 140}]}> + <SearchLanguageDropdown + value={params.lang} + onChange={params.setLang} + /> + </View> + </View> + )} </View> - )} - </CenteredView> + </Layout.Center> + </View> <View style={{ @@ -992,10 +974,7 @@ let AutocompleteResults = ({ !moderationOpts ? ( <Loader /> ) : ( - <ScrollView - style={{height: '100%'}} - // @ts-ignore web only -prf - dataSet={{stableGutters: '1'}} + <Layout.Content keyboardShouldPersistTaps="handled" keyboardDismissMode="on-drag"> <SearchLinkCard @@ -1020,7 +999,7 @@ let AutocompleteResults = ({ /> ))} <View style={{height: 200}} /> - </ScrollView> + </Layout.Content> )} </> ) @@ -1042,17 +1021,12 @@ function SearchHistory({ onRemoveItemClick: (item: string) => void onRemoveProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void }) { - const {isTabletOrDesktop, isMobile} = useWebMediaQueries() + const {isMobile} = useWebMediaQueries() const pal = usePalette('default') const {_} = useLingui() return ( - <CenteredView - sideBorders={isTabletOrDesktop} - // @ts-ignore web only -prf - style={{ - height: isWeb ? '100vh' : undefined, - }}> + <Layout.Content> <View style={styles.searchHistoryContainer}> {(searchHistory.length > 0 || selectedProfiles.length > 0) && ( <Text style={[pal.text, styles.searchHistoryTitle]}> @@ -1152,7 +1126,7 @@ function SearchHistory({ </View> )} </View> - </CenteredView> + </Layout.Content> ) } diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx index 9f407248a..47a86ed24 100644 --- a/src/view/shell/Composer.web.tsx +++ b/src/view/shell/Composer.web.tsx @@ -3,8 +3,8 @@ import {StyleSheet, View} from 'react-native' import {DismissableLayer} from '@radix-ui/react-dismissable-layer' import {useFocusGuards} from '@radix-ui/react-focus-guards' import {FocusScope} from '@radix-ui/react-focus-scope' +import {RemoveScrollBar} from 'react-remove-scroll-bar' -import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' import {useModals} from '#/state/modals' import {ComposerOpts, useComposerState} from '#/state/shell/composer' import { @@ -20,8 +20,6 @@ export function Composer({}: {winHeight: number}) { const state = useComposerState() const isActive = !!state - useWebBodyScrollLock(isActive) - // rendering // = @@ -29,7 +27,12 @@ export function Composer({}: {winHeight: number}) { return <View /> } - return <Inner state={state} /> + return ( + <> + <RemoveScrollBar /> + <Inner state={state} /> + </> + ) } function Inner({state}: {state: ComposerOpts}) { diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 0af80854c..7c2ccd958 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -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 @@ -220,35 +176,44 @@ function NavItem({count, href, icon, iconFilled, label}: NavItemProps) { ]}> {isCurrent ? iconFilled : icon} {typeof count === 'string' && count ? ( - <Text - accessibilityLabel={_(msg`${count} unread items`)} - accessibilityHint="" - accessible={true} + <View style={[ a.absolute, - a.text_xs, - a.font_bold, - a.rounded_full, - a.text_center, - { - top: '-10%', - left: count.length === 1 ? '50%' : '40%', - backgroundColor: t.palette.primary_500, - color: t.palette.white, - lineHeight: a.text_sm.fontSize, - paddingHorizontal: 4, - paddingVertical: 1, - minWidth: 16, - }, - isTablet && [ + a.inset_0, + {right: -20}, // more breathing room + ]}> + <Text + accessibilityLabel={_(msg`${count} unread items`)} + accessibilityHint="" + accessible={true} + numberOfLines={1} + style={[ + a.absolute, + a.text_xs, + a.font_bold, + a.rounded_full, + a.text_center, + a.leading_tight, { - top: '10%', - left: count.length === 1 ? '50%' : '40%', + top: '-10%', + left: count.length === 1 ? 12 : 8, + backgroundColor: t.palette.primary_500, + color: t.palette.white, + lineHeight: a.text_sm.fontSize, + paddingHorizontal: 4, + paddingVertical: 1, + minWidth: 16, }, - ], - ]}> - {count} - </Text> + isTablet && [ + { + top: '10%', + left: count.length === 1 ? 20 : 16, + }, + ], + ]}> + {count} + </Text> + </View> ) : null} </View> {gtTablet && ( @@ -366,9 +331,9 @@ export function DesktopLeftNav() { <View role="navigation" style={[ + a.px_xl, styles.leftNav, isTablet && styles.leftNavTablet, - pal.view, pal.border, ]}> {hasSession ? ( @@ -381,8 +346,6 @@ export function DesktopLeftNav() { {hasSession && ( <> - <BackBtn /> - <NavItem href="/" icon={ @@ -525,8 +488,17 @@ const styles = StyleSheet.create({ position: 'fixed', top: 10, // @ts-ignore web only - left: 'calc(50vw - 300px - 220px - 20px)', - width: 220, + left: '50%', + transform: [ + { + translateX: -300, + }, + { + translateX: '-100%', + }, + ...a.scrollbar_offset.transform, + ], + width: 240, // @ts-ignore web only maxHeight: 'calc(100vh - 10px)', overflowY: 'auto', @@ -538,7 +510,10 @@ const styles = StyleSheet.create({ borderRightWidth: 1, height: '100%', width: 76, + paddingLeft: 0, + paddingRight: 0, alignItems: 'center', + transform: [], }, profileCard: { diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index 4f413211f..7814f3548 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -28,7 +28,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) { } return ( - <View style={[styles.rightNav, pal.view]}> + <View style={[a.px_xl, styles.rightNav]}> <View style={{paddingVertical: 20}}> {routeName === 'Search' ? ( <View style={{marginBottom: 18}}> @@ -122,8 +122,13 @@ const styles = StyleSheet.create({ // @ts-ignore web only position: 'fixed', // @ts-ignore web only - left: 'calc(50vw + 300px + 20px)', - width: 300, + left: '50%', + transform: [ + { + translateX: 300, + }, + ...a.scrollbar_offset.transform, + ], maxHeight: '100%', overflowY: 'auto', }, diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index f55437356..8c30813ab 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -3,10 +3,10 @@ import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' +import {RemoveScrollBar} from 'react-remove-scroll-bar' import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' import {useIntentHandler} from '#/lib/hooks/useIntentHandler' -import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {NavigationProp} from '#/lib/routes/types' import {colors} from '#/lib/styles' @@ -34,7 +34,6 @@ function ShellInner() { const {_} = useLingui() const showDrawer = !isDesktop && isDrawerOpen - useWebBodyScrollLock(showDrawer) useComposerKeyboardShortcut() useIntentHandler() @@ -58,31 +57,34 @@ function ShellInner() { <PortalOutlet /> {showDrawer && ( - <TouchableWithoutFeedback - onPress={ev => { - // Only close if press happens outside of the drawer - if (ev.target === ev.currentTarget) { - setDrawerOpen(false) - } - }} - accessibilityLabel={_(msg`Close navigation footer`)} - accessibilityHint={_(msg`Closes bottom navigation bar`)}> - <View - style={[ - styles.drawerMask, - { - backgroundColor: select(t.name, { - light: 'rgba(0, 57, 117, 0.1)', - dark: 'rgba(1, 82, 168, 0.1)', - dim: 'rgba(10, 13, 16, 0.8)', - }), - }, - ]}> - <View style={styles.drawerContainer}> - <DrawerContent /> + <> + <RemoveScrollBar /> + <TouchableWithoutFeedback + onPress={ev => { + // Only close if press happens outside of the drawer + if (ev.target === ev.currentTarget) { + setDrawerOpen(false) + } + }} + accessibilityLabel={_(msg`Close navigation footer`)} + accessibilityHint={_(msg`Closes bottom navigation bar`)}> + <View + style={[ + styles.drawerMask, + { + backgroundColor: select(t.name, { + light: 'rgba(0, 57, 117, 0.1)', + dark: 'rgba(1, 82, 168, 0.1)', + dim: 'rgba(10, 13, 16, 0.8)', + }), + }, + ]}> + <View style={styles.drawerContainer}> + <DrawerContent /> + </View> </View> - </View> - </TouchableWithoutFeedback> + </TouchableWithoutFeedback> + </> )} </> ) diff --git a/web/index.html b/web/index.html index 28b3a3e3d..293f366ad 100644 --- a/web/index.html +++ b/web/index.html @@ -45,7 +45,6 @@ } html { background-color: white; - scrollbar-gutter: stable both-edges; } @media (prefers-color-scheme: dark) { html { @@ -81,9 +80,15 @@ top: 50%; transform: translateX(-50%) translateY(-50%) translateY(-50px); } - /* We need this style to prevent web dropdowns from shifting the display when opening */ + /** + * We need these styles to prevent shifting due to scrollbar show/hide on + * OSs that have them enabled by default. This also handles cases where the + * screen wouldn't otherwise scroll, and therefore hide the scrollbar and + * shift the content, by forcing the page to show a scrollbar. + */ body { width: 100%; + overflow-y: scroll; } </style> </head> diff --git a/yarn.lock b/yarn.lock index 474ef2f8b..e62dcb97f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17616,16 +17616,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -17725,7 +17716,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -17739,13 +17730,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -19068,7 +19052,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -19086,15 +19070,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" |