deer social fork for personal usage. but you might see a use idk. github mirror

[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>

authored by Eric Bailey Samuel Newman and committed by GitHub 143e2c80 8467dfd4

Changed files
+1721 -2048
assets
bskyweb
templates
src
alf
components
lib
screens
view
web
+1
assets/icons/floppyDisk_stroke2_corner0_rounded.svg
··· 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>
+7 -2
bskyweb/templates/base.html
··· 40 40 } 41 41 html { 42 42 background-color: white; 43 - scrollbar-gutter: stable both-edges; 44 43 } 45 44 @media (prefers-color-scheme: dark) { 46 45 html { ··· 76 75 top: 50%; 77 76 transform: translateX(-50%) translateY(-50%) translateY(-50px); 78 77 } 79 - /* We need this style to prevent web dropdowns from shifting the display when opening */ 78 + /** 79 + * We need these styles to prevent shifting due to scrollbar show/hide on 80 + * OSs that have them enabled by default. This also handles cases where the 81 + * screen wouldn't otherwise scroll, and therefore hide the scrollbar and 82 + * shift the content, by forcing the page to show a scrollbar. 83 + */ 80 84 body { 81 85 width: 100%; 86 + overflow-y: scroll; 82 87 } 83 88 </style> 84 89
+1
package.json
··· 193 193 "react-native-web": "~0.19.11", 194 194 "react-native-web-webview": "^1.0.2", 195 195 "react-native-webview": "13.10.2", 196 + "react-remove-scroll-bar": "^2.3.6", 196 197 "react-responsive": "^9.0.2", 197 198 "react-textarea-autosize": "^8.5.3", 198 199 "rn-fetch-blob": "^0.12.0",
+21 -1
src/alf/atoms.ts
··· 1 1 import {Platform, StyleProp, StyleSheet, ViewStyle} from 'react-native' 2 2 3 3 import * as tokens from '#/alf/tokens' 4 - import {ios, native, web} from '#/alf/util/platform' 4 + import {ios, native, platform, web} from '#/alf/util/platform' 5 + import * as Layout from '#/components/Layout' 5 6 6 7 export const atoms = { 7 8 debug: { ··· 21 22 relative: { 22 23 position: 'relative', 23 24 }, 25 + sticky: web({ 26 + position: 'sticky', 27 + }), 24 28 inset_0: { 25 29 top: 0, 26 30 left: 0, ··· 941 945 transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)', 942 946 transitionDuration: '100ms', 943 947 }), 948 + 949 + /** 950 + * {@link Layout.SCROLLBAR_OFFSET} 951 + */ 952 + scrollbar_offset: platform({ 953 + web: { 954 + transform: [ 955 + { 956 + translateX: Layout.SCROLLBAR_OFFSET, 957 + }, 958 + ], 959 + }, 960 + native: { 961 + transform: [], 962 + }, 963 + }) as {transform: Exclude<ViewStyle['transform'], string | undefined>}, 944 964 } as const
+1
src/alf/index.tsx
··· 20 20 export * from '#/alf/util/flatten' 21 21 export * from '#/alf/util/platform' 22 22 export * from '#/alf/util/themeSelector' 23 + export * from '#/alf/util/useGutterStyles' 23 24 24 25 export type Alf = { 25 26 themeName: ThemeName
+21
src/alf/util/useGutterStyles.ts
··· 1 + import React from 'react' 2 + 3 + import {atoms as a, useBreakpoints, ViewStyleProp} from '#/alf' 4 + 5 + export function useGutterStyles({ 6 + top, 7 + bottom, 8 + }: { 9 + top?: boolean 10 + bottom?: boolean 11 + } = {}) { 12 + const {gtMobile} = useBreakpoints() 13 + return React.useMemo<ViewStyleProp['style']>(() => { 14 + return [ 15 + a.px_lg, 16 + top && a.pt_md, 17 + bottom && a.pb_md, 18 + gtMobile && [a.px_xl, top && a.pt_lg, bottom && a.pb_lg], 19 + ] 20 + }, [gtMobile, top, bottom]) 21 + }
+2
src/components/Dialog/index.web.tsx
··· 12 12 import {DismissableLayer} from '@radix-ui/react-dismissable-layer' 13 13 import {useFocusGuards} from '@radix-ui/react-focus-guards' 14 14 import {FocusScope} from '@radix-ui/react-focus-scope' 15 + import {RemoveScrollBar} from 'react-remove-scroll-bar' 15 16 16 17 import {logger} from '#/logger' 17 18 import {useDialogStateControlContext} from '#/state/dialogs' ··· 103 104 {isOpen && ( 104 105 <Portal> 105 106 <Context.Provider value={context}> 107 + <RemoveScrollBar /> 106 108 <TouchableWithoutFeedback 107 109 accessibilityHint={undefined} 108 110 accessibilityLabel={_(msg`Close active dialog`)}
-100
src/components/Layout.tsx
··· 1 - import React, {useContext, useMemo} from 'react' 2 - import {View, ViewStyle} from 'react-native' 3 - import {StyleProp} from 'react-native' 4 - import {useSafeAreaInsets} from 'react-native-safe-area-context' 5 - 6 - import {ViewHeader} from '#/view/com/util/ViewHeader' 7 - import {ScrollView} from '#/view/com/util/Views' 8 - import {CenteredView} from '#/view/com/util/Views' 9 - import {atoms as a} from '#/alf' 10 - 11 - // Every screen should have a Layout component wrapping it. 12 - // This component provides a default padding for the top of the screen. 13 - // This allows certain screens to avoid the top padding if they want to. 14 - 15 - const LayoutContext = React.createContext({ 16 - withinScreen: false, 17 - topPaddingDisabled: false, 18 - withinScrollView: false, 19 - }) 20 - 21 - /** 22 - * Every screen should have a Layout.Screen component wrapping it. 23 - * This component provides a default padding for the top of the screen 24 - * and height/minHeight 25 - */ 26 - let Screen = ({ 27 - disableTopPadding = false, 28 - style, 29 - ...props 30 - }: React.ComponentProps<typeof View> & { 31 - disableTopPadding?: boolean 32 - style?: StyleProp<ViewStyle> 33 - }): React.ReactNode => { 34 - const {top} = useSafeAreaInsets() 35 - const context = useMemo( 36 - () => ({ 37 - withinScreen: true, 38 - topPaddingDisabled: disableTopPadding, 39 - withinScrollView: false, 40 - }), 41 - [disableTopPadding], 42 - ) 43 - return ( 44 - <LayoutContext.Provider value={context}> 45 - <View 46 - style={[ 47 - {paddingTop: disableTopPadding ? 0 : top}, 48 - a.util_screen_outer, 49 - style, 50 - ]} 51 - {...props} 52 - /> 53 - </LayoutContext.Provider> 54 - ) 55 - } 56 - Screen = React.memo(Screen) 57 - export {Screen} 58 - 59 - let Header = ( 60 - props: React.ComponentProps<typeof ViewHeader>, 61 - ): React.ReactNode => { 62 - const {withinScrollView} = useContext(LayoutContext) 63 - if (!withinScrollView) { 64 - return ( 65 - <CenteredView topBorder={false} sideBorders> 66 - <ViewHeader showOnDesktop showBorder {...props} /> 67 - </CenteredView> 68 - ) 69 - } else { 70 - return <ViewHeader showOnDesktop showBorder {...props} /> 71 - } 72 - } 73 - Header = React.memo(Header) 74 - export {Header} 75 - 76 - let Content = ({ 77 - style, 78 - contentContainerStyle, 79 - ...props 80 - }: React.ComponentProps<typeof ScrollView> & { 81 - style?: StyleProp<ViewStyle> 82 - contentContainerStyle?: StyleProp<ViewStyle> 83 - }): React.ReactNode => { 84 - const context = useContext(LayoutContext) 85 - const newContext = useMemo( 86 - () => ({...context, withinScrollView: true}), 87 - [context], 88 - ) 89 - return ( 90 - <LayoutContext.Provider value={newContext}> 91 - <ScrollView 92 - style={[a.flex_1, style]} 93 - contentContainerStyle={[{paddingBottom: 100}, contentContainerStyle]} 94 - {...props} 95 - /> 96 - </LayoutContext.Provider> 97 - ) 98 - } 99 - Content = React.memo(Content) 100 - export {Content}
+199
src/components/Layout/Header/index.tsx
··· 1 + import {createContext, useCallback, useContext} from 'react' 2 + import {GestureResponderEvent, View} from 'react-native' 3 + import {msg} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + import {useNavigation} from '@react-navigation/native' 6 + 7 + import {HITSLOP_30} from '#/lib/constants' 8 + import {NavigationProp} from '#/lib/routes/types' 9 + import {isIOS} from '#/platform/detection' 10 + import {useSetDrawerOpen} from '#/state/shell' 11 + import { 12 + atoms as a, 13 + platform, 14 + TextStyleProp, 15 + useBreakpoints, 16 + useGutterStyles, 17 + useTheme, 18 + } from '#/alf' 19 + import {Button, ButtonIcon, ButtonProps} from '#/components/Button' 20 + import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow' 21 + import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' 22 + import { 23 + BUTTON_VISUAL_ALIGNMENT_OFFSET, 24 + HEADER_SLOT_SIZE, 25 + } from '#/components/Layout/const' 26 + import {ScrollbarOffsetContext} from '#/components/Layout/context' 27 + import {Text} from '#/components/Typography' 28 + 29 + export function Outer({ 30 + children, 31 + noBottomBorder, 32 + }: { 33 + children: React.ReactNode 34 + noBottomBorder?: boolean 35 + }) { 36 + const t = useTheme() 37 + const gutter = useGutterStyles() 38 + const {gtMobile} = useBreakpoints() 39 + const {isWithinOffsetView} = useContext(ScrollbarOffsetContext) 40 + 41 + return ( 42 + <View 43 + style={[ 44 + a.w_full, 45 + !noBottomBorder && a.border_b, 46 + a.flex_row, 47 + a.align_center, 48 + a.gap_sm, 49 + gutter, 50 + platform({ 51 + native: [a.pb_sm, a.pt_xs], 52 + web: [a.py_sm], 53 + }), 54 + t.atoms.border_contrast_low, 55 + gtMobile && [a.mx_auto, {maxWidth: 600}], 56 + !isWithinOffsetView && a.scrollbar_offset, 57 + ]}> 58 + {children} 59 + </View> 60 + ) 61 + } 62 + 63 + const AlignmentContext = createContext<'platform' | 'left'>('platform') 64 + 65 + export function Content({ 66 + children, 67 + align = 'platform', 68 + }: { 69 + children?: React.ReactNode 70 + align?: 'platform' | 'left' 71 + }) { 72 + return ( 73 + <View 74 + style={[ 75 + a.flex_1, 76 + a.justify_center, 77 + isIOS && align === 'platform' && a.align_center, 78 + {minHeight: HEADER_SLOT_SIZE}, 79 + ]}> 80 + <AlignmentContext.Provider value={align}> 81 + {children} 82 + </AlignmentContext.Provider> 83 + </View> 84 + ) 85 + } 86 + 87 + export function Slot({children}: {children?: React.ReactNode}) { 88 + return ( 89 + <View 90 + style={[ 91 + a.z_50, 92 + { 93 + width: HEADER_SLOT_SIZE, 94 + }, 95 + ]}> 96 + {children} 97 + </View> 98 + ) 99 + } 100 + 101 + export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) { 102 + const {_} = useLingui() 103 + const navigation = useNavigation<NavigationProp>() 104 + 105 + const onPressBack = useCallback( 106 + (evt: GestureResponderEvent) => { 107 + onPress?.(evt) 108 + if (evt.defaultPrevented) return 109 + if (navigation.canGoBack()) { 110 + navigation.goBack() 111 + } else { 112 + navigation.navigate('Home') 113 + } 114 + }, 115 + [onPress, navigation], 116 + ) 117 + 118 + return ( 119 + <Slot> 120 + <Button 121 + label={_(msg`Go back`)} 122 + size="small" 123 + variant="ghost" 124 + color="secondary" 125 + shape="square" 126 + onPress={onPressBack} 127 + hitSlop={HITSLOP_30} 128 + style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}, style]} 129 + {...props}> 130 + <ButtonIcon icon={ArrowLeft} size="lg" /> 131 + </Button> 132 + </Slot> 133 + ) 134 + } 135 + 136 + export function MenuButton() { 137 + const {_} = useLingui() 138 + const setDrawerOpen = useSetDrawerOpen() 139 + const {gtMobile} = useBreakpoints() 140 + 141 + const onPress = useCallback(() => { 142 + setDrawerOpen(true) 143 + }, [setDrawerOpen]) 144 + 145 + return gtMobile ? null : ( 146 + <Slot> 147 + <Button 148 + label={_(msg`Open drawer menu`)} 149 + size="small" 150 + variant="ghost" 151 + color="secondary" 152 + shape="square" 153 + onPress={onPress} 154 + hitSlop={HITSLOP_30} 155 + style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}]}> 156 + <ButtonIcon icon={Menu} size="lg" /> 157 + </Button> 158 + </Slot> 159 + ) 160 + } 161 + 162 + export function TitleText({ 163 + children, 164 + style, 165 + }: {children: React.ReactNode} & TextStyleProp) { 166 + const {gtMobile} = useBreakpoints() 167 + const align = useContext(AlignmentContext) 168 + return ( 169 + <Text 170 + style={[ 171 + a.text_lg, 172 + a.font_heavy, 173 + a.leading_tight, 174 + isIOS && align === 'platform' && a.text_center, 175 + gtMobile && a.text_xl, 176 + style, 177 + ]} 178 + numberOfLines={2}> 179 + {children} 180 + </Text> 181 + ) 182 + } 183 + 184 + export function SubtitleText({children}: {children: React.ReactNode}) { 185 + const t = useTheme() 186 + const align = useContext(AlignmentContext) 187 + return ( 188 + <Text 189 + style={[ 190 + a.text_sm, 191 + a.leading_snug, 192 + isIOS && align === 'platform' && a.text_center, 193 + t.atoms.text_contrast_medium, 194 + ]} 195 + numberOfLines={2}> 196 + {children} 197 + </Text> 198 + ) 199 + }
+172
src/components/Layout/README.md
··· 1 + # Layout 2 + 3 + This directory contains our core layout components. Use these when creating new 4 + screens, or when supplementing other components with functionality like 5 + centering. 6 + 7 + ## Usage 8 + 9 + If we aren't talking about the `shell` components, layouts on individual screens 10 + look like more or less like this: 11 + 12 + ```tsx 13 + <Outer> 14 + <Header>...</Header> 15 + <Content>...</Content> 16 + </Outer> 17 + ``` 18 + 19 + I'll map these words to real components. 20 + 21 + ### `Layout.Screen` 22 + 23 + Provides the "Outer" functionality for a screen, like taking up the full height 24 + of the screen. **All screens should be wrapped with this component,** probably 25 + as the outermost component. 26 + 27 + > [!NOTE] 28 + > On web, `Layout.Screen` also provides the side borders on our central content 29 + > column. These borders are fixed position, 1px outside our center column width 30 + > of 600px. 31 + > 32 + > What this effectively means is that _nothing inside the center content column 33 + > needs (or should) define left/right borders._ That is now handled in one 34 + > place: within `Layout.Screen`. 35 + 36 + ### `Layout.Header.*` 37 + 38 + The `Layout.Header` component actually contains multiple sub-components. Use 39 + this to compose different versions of the header. The most basic version looks 40 + like this: 41 + 42 + ```tsx 43 + <Layout.Header.Outer> 44 + <Layout.Header.BackButton /> {/* or <Layout.Header.MenuButton /> */} 45 + 46 + <Layout.Header.Content> 47 + <Layout.Header.TitleText>Account</Layout.Header.TitleText> 48 + 49 + {/* Optional subtitle */} 50 + <Layout.Header.SubtitleText>Settings for @esb.lol</Layout.Header.SubtitleText> 51 + </Layout.Header.Content> 52 + 53 + <Layout.Header.Slot /> 54 + </Layout.Header.Outer> 55 + ``` 56 + 57 + Note the additional `Slot` component. This is here to keep the header balanced 58 + and provide correct spacing on all platforms. The `Slot` is 34px wide, which 59 + matches the `BackButton` and `MenuButton`. 60 + 61 + > If anyone has better ideas, I'm all ears, but this was simple and the small 62 + > amount of boilerplate is only incurred when creating a new screen, which is 63 + > infrequent. 64 + 65 + It can also function as a "slot" for a button positioned on the right side. See 66 + the `Hashtag` screen for an example, abbreviated below: 67 + 68 + ```tsx 69 + <Layout.Header.Slot> 70 + <Button size='small' shape='round'>...</Button> 71 + </Layout.Header.Slot> 72 + ``` 73 + 74 + If you need additional customization, simply use the components that are helpful 75 + and create new ones as needed. A good example is the `SavedFeeds` screen, which 76 + looks roughly like this: 77 + 78 + ```tsx 79 + <Layout.Header.Outer> 80 + <Layout.Header.BackButton /> 81 + 82 + {/* Override to align content to the left, making room for the button */} 83 + <Layout.Header.Content align='left'> 84 + <Layout.Header.TitleText>Edit My Feeds</Layout.Header.TitleText> 85 + </Layout.Header.Content> 86 + 87 + {/* Custom button, wider than 34px */} 88 + <Button size='small'>...</Button> 89 + </Layout.Header.Outer> 90 + ``` 91 + 92 + > [!TIP] 93 + > The `Header` should be _outside_ the `Content` component in order to be 94 + > fixed on scroll on native. Placing it inside will make it scroll with the rest 95 + > of the page. 96 + 97 + ### `Layout.Content` 98 + 99 + This provides the "Content" functionality for a screen. This component is 100 + actually an `Animated.ScrollView`, and accepts props for that component. It 101 + provides a little default styling as well. On web, it also _centers the content 102 + inside our center content column of 600px_. 103 + 104 + > [!NOTE] 105 + > What about flatlists or pagers? Those components are not colocated here (yet). 106 + > But those components serve the same purpose of "Content". 107 + 108 + ## Examples 109 + 110 + The most basic layout available to us looks like this: 111 + 112 + ```tsx 113 + <Layout.Screen> 114 + <Layout.Header.Outer> 115 + <Layout.Header.BackButton /> {/* or <Layout.Header.MenuButton /> */} 116 + 117 + <Layout.Header.Content> 118 + <Layout.Header.TitleText>Account</Layout.Header.TitleText> 119 + 120 + {/* Optional subtitle */} 121 + <Layout.Header.SubtitleText>Settings for @esb.lol</Layout.Header.SubtitleText> 122 + </Layout.Header.Content> 123 + 124 + <Layout.Header.Slot /> 125 + </Layout.Header.Outer> 126 + 127 + <Layout.Content> 128 + ... 129 + </Layout.Content> 130 + </Layout.Screen> 131 + ``` 132 + 133 + **For `List` views,** you'd sub in `List` for `Layout.Content` and it will 134 + function the same. See `Feeds` screen for an example. 135 + 136 + **For `Pager` views,** including `PagerWithHeader`, do the same. See `Hashtag` 137 + screen for an example. 138 + 139 + ## Utilities 140 + 141 + ### `Layout.Center` 142 + 143 + This component behaves like our old `CenteredView` component. 144 + 145 + ### `Layout.SCROLLBAR_OFFSET` and `Layout.SCROLLBAR_OFFSET_POSITIVE` 146 + 147 + Provide a pre-configured CSS vars for use when aligning fixed position elements. 148 + More on this below. 149 + 150 + ## Scrollbar gutter handling 151 + 152 + Operating systems allow users to configure if their browser _always_ shows 153 + scrollbars not. Some OSs also don't allow configuration. 154 + 155 + The presence of scrollbars affects layout, particularly fixed position elements. 156 + Browsers support `scrollbar-gutter`, but each behaves differently. Our approach 157 + is to use the default `scrollbar-gutter: auto`. Basically, we start from a clean 158 + slate. 159 + 160 + This handling becomes particularly thorny when we need to lock scroll, like when 161 + opening a dialog or dropdown. Radix uses the library `react-remove-scroll` 162 + internally, which in turn depends on 163 + [`react-remove-scroll-bar`](https://github.com/theKashey/react-remove-scroll-bar). 164 + We've opted to rely on this transient dependency. This library adds some utility 165 + classes and CSS vars to the page when scroll is locked. 166 + 167 + **It is this CSS variable that we use in `SCROLLBAR_OFFSET` values.** This 168 + ensures that elements do not shift relative to the screen when opening a 169 + dropdown or dialog. 170 + 171 + These styles are applied where needed and we should have very little need of 172 + adjusting them often.
+16
src/components/Layout/const.ts
··· 1 + export const SCROLLBAR_OFFSET = 2 + 'calc(-1 * var(--removed-body-scroll-bar-size, 0px) / 2)' as any 3 + export const SCROLLBAR_OFFSET_POSITIVE = 4 + 'calc(var(--removed-body-scroll-bar-size, 0px) / 2)' as any 5 + 6 + /** 7 + * Useful for visually aligning icons within header buttons with the elements 8 + * below them on the screen. Apply positively or negatively depending on side 9 + * of the screen you're on. 10 + */ 11 + export const BUTTON_VISUAL_ALIGNMENT_OFFSET = 3 12 + 13 + /** 14 + * Corresponds to the width of a small square or round button 15 + */ 16 + export const HEADER_SLOT_SIZE = 34
+5
src/components/Layout/context.ts
··· 1 + import React from 'react' 2 + 3 + export const ScrollbarOffsetContext = React.createContext({ 4 + isWithinOffsetView: false, 5 + })
+188
src/components/Layout/index.tsx
··· 1 + import React, {useContext, useMemo} from 'react' 2 + import {StyleSheet, View, ViewProps, ViewStyle} from 'react-native' 3 + import {StyleProp} from 'react-native' 4 + import { 5 + KeyboardAwareScrollView, 6 + KeyboardAwareScrollViewProps, 7 + } from 'react-native-keyboard-controller' 8 + import Animated, { 9 + AnimatedScrollViewProps, 10 + useAnimatedProps, 11 + } from 'react-native-reanimated' 12 + import {useSafeAreaInsets} from 'react-native-safe-area-context' 13 + 14 + import {isWeb} from '#/platform/detection' 15 + import {useShellLayout} from '#/state/shell/shell-layout' 16 + import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 17 + import {ScrollbarOffsetContext} from '#/components/Layout/context' 18 + 19 + export * from '#/components/Layout/const' 20 + export * as Header from '#/components/Layout/Header' 21 + 22 + export type ScreenProps = React.ComponentProps<typeof View> & { 23 + style?: StyleProp<ViewStyle> 24 + } 25 + 26 + /** 27 + * Outermost component of every screen 28 + */ 29 + export const Screen = React.memo(function Screen({ 30 + style, 31 + ...props 32 + }: ScreenProps) { 33 + const {top} = useSafeAreaInsets() 34 + return ( 35 + <> 36 + {isWeb && <WebCenterBorders />} 37 + <View 38 + style={[a.util_screen_outer, {paddingTop: top}, style]} 39 + {...props} 40 + /> 41 + </> 42 + ) 43 + }) 44 + 45 + export type ContentProps = AnimatedScrollViewProps & { 46 + style?: StyleProp<ViewStyle> 47 + contentContainerStyle?: StyleProp<ViewStyle> 48 + } 49 + 50 + /** 51 + * Default scroll view for simple pages 52 + */ 53 + export const Content = React.memo(function Content({ 54 + children, 55 + style, 56 + contentContainerStyle, 57 + ...props 58 + }: ContentProps) { 59 + const {footerHeight} = useShellLayout() 60 + const animatedProps = useAnimatedProps(() => { 61 + return { 62 + scrollIndicatorInsets: { 63 + bottom: footerHeight.get(), 64 + top: 0, 65 + right: 1, 66 + }, 67 + } satisfies AnimatedScrollViewProps 68 + }) 69 + 70 + return ( 71 + <Animated.ScrollView 72 + id="content" 73 + automaticallyAdjustsScrollIndicatorInsets={false} 74 + // sets the scroll inset to the height of the footer 75 + animatedProps={animatedProps} 76 + style={[scrollViewStyles.common, style]} 77 + contentContainerStyle={[ 78 + scrollViewStyles.contentContainer, 79 + contentContainerStyle, 80 + ]} 81 + {...props}> 82 + {isWeb ? ( 83 + // @ts-ignore web only -esb 84 + <Center>{children}</Center> 85 + ) : ( 86 + children 87 + )} 88 + </Animated.ScrollView> 89 + ) 90 + }) 91 + 92 + const scrollViewStyles = StyleSheet.create({ 93 + common: { 94 + width: '100%', 95 + }, 96 + contentContainer: { 97 + paddingBottom: 100, 98 + }, 99 + }) 100 + 101 + export type KeyboardAwareContentProps = KeyboardAwareScrollViewProps & { 102 + children: React.ReactNode 103 + contentContainerStyle?: StyleProp<ViewStyle> 104 + } 105 + 106 + /** 107 + * Default scroll view for simple pages. 108 + * 109 + * BE SURE TO TEST THIS WHEN USING, it's untested as of writing this comment. 110 + */ 111 + export const KeyboardAwareContent = React.memo(function LayoutScrollView({ 112 + children, 113 + style, 114 + contentContainerStyle, 115 + ...props 116 + }: KeyboardAwareContentProps) { 117 + return ( 118 + <KeyboardAwareScrollView 119 + style={[scrollViewStyles.common, style]} 120 + contentContainerStyle={[ 121 + scrollViewStyles.contentContainer, 122 + contentContainerStyle, 123 + ]} 124 + keyboardShouldPersistTaps="handled" 125 + {...props}> 126 + {isWeb ? <Center>{children}</Center> : children} 127 + </KeyboardAwareScrollView> 128 + ) 129 + }) 130 + 131 + /** 132 + * Utility component to center content within the screen 133 + */ 134 + export const Center = React.memo(function LayoutContent({ 135 + children, 136 + style, 137 + ...props 138 + }: ViewProps) { 139 + const {isWithinOffsetView} = useContext(ScrollbarOffsetContext) 140 + const {gtMobile} = useBreakpoints() 141 + const ctx = useMemo(() => ({isWithinOffsetView: true}), []) 142 + return ( 143 + <View 144 + style={[ 145 + a.w_full, 146 + a.mx_auto, 147 + gtMobile && { 148 + maxWidth: 600, 149 + }, 150 + style, 151 + !isWithinOffsetView && a.scrollbar_offset, 152 + ]} 153 + {...props}> 154 + <ScrollbarOffsetContext.Provider value={ctx}> 155 + {children} 156 + </ScrollbarOffsetContext.Provider> 157 + </View> 158 + ) 159 + }) 160 + 161 + /** 162 + * Only used within `Layout.Screen`, not for reuse 163 + */ 164 + const WebCenterBorders = React.memo(function LayoutContent() { 165 + const t = useTheme() 166 + const {gtMobile} = useBreakpoints() 167 + return gtMobile ? ( 168 + <View 169 + style={[ 170 + a.fixed, 171 + a.inset_0, 172 + a.border_l, 173 + a.border_r, 174 + t.atoms.border_contrast_low, 175 + web({ 176 + width: 602, 177 + left: '50%', 178 + transform: [ 179 + { 180 + translateX: '-50%', 181 + }, 182 + ...a.scrollbar_offset.transform, 183 + ], 184 + }), 185 + ]} 186 + /> 187 + ) : null 188 + })
+11 -2
src/components/LikedByList.tsx
··· 12 12 import {List} from '#/view/com/util/List' 13 13 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 14 14 15 - function renderItem({item}: {item: GetLikes.Like}) { 16 - return <ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} /> 15 + function renderItem({item, index}: {item: GetLikes.Like; index: number}) { 16 + return ( 17 + <ProfileCardWithFollowBtn 18 + key={item.actor.did} 19 + profile={item.actor} 20 + noBorder={index === 0} 21 + /> 22 + ) 17 23 } 18 24 19 25 function keyExtractor(item: GetLikes.Like) { ··· 81 87 )} 82 88 errorMessage={cleanError(resolveError || error)} 83 89 onRetry={isError ? refetch : undefined} 90 + topBorder={false} 91 + sideBorders={false} 84 92 /> 85 93 ) 86 94 } ··· 103 111 onEndReachedThreshold={3} 104 112 initialNumToRender={initialNumToRender} 105 113 windowSize={11} 114 + sideBorders={false} 106 115 /> 107 116 ) 108 117 }
+1 -33
src/components/Lists.tsx
··· 109 109 ) 110 110 } 111 111 112 - export function ListHeaderDesktop({ 113 - title, 114 - subtitle, 115 - }: { 116 - title: string 117 - subtitle?: string 118 - }) { 119 - const {gtTablet} = useBreakpoints() 120 - const t = useTheme() 121 - 122 - if (!gtTablet) return null 123 - 124 - return ( 125 - <View 126 - style={[ 127 - a.w_full, 128 - a.py_sm, 129 - a.px_xl, 130 - a.gap_xs, 131 - a.justify_center, 132 - {minHeight: 50}, 133 - ]}> 134 - <Text style={[a.text_2xl, a.font_bold]}>{title}</Text> 135 - {subtitle ? ( 136 - <Text style={[a.text_md, t.atoms.text_contrast_medium]}> 137 - {subtitle} 138 - </Text> 139 - ) : undefined} 140 - </View> 141 - ) 142 - } 143 - 144 112 let ListMaybePlaceholder = ({ 145 113 isLoading, 146 114 noEmpty, ··· 154 122 onGoBack, 155 123 hideBackButton, 156 124 sideBorders, 157 - topBorder = true, 125 + topBorder = false, 158 126 }: { 159 127 isLoading: boolean 160 128 noEmpty?: boolean
+17 -19
src/components/dms/MessagesListHeader.tsx
··· 65 65 a.pr_lg, 66 66 a.py_sm, 67 67 ]}> 68 - {!gtTablet && ( 69 - <TouchableOpacity 70 - testID="conversationHeaderBackBtn" 71 - onPress={onPressBack} 72 - hitSlop={BACK_HITSLOP} 73 - style={{width: 30, height: 30, marginTop: isWeb ? 6 : 4}} 74 - accessibilityRole="button" 75 - accessibilityLabel={_(msg`Back`)} 76 - accessibilityHint=""> 77 - <FontAwesomeIcon 78 - size={18} 79 - icon="angle-left" 80 - style={{ 81 - marginTop: 6, 82 - }} 83 - color={t.atoms.text.color} 84 - /> 85 - </TouchableOpacity> 86 - )} 68 + <TouchableOpacity 69 + testID="conversationHeaderBackBtn" 70 + onPress={onPressBack} 71 + hitSlop={BACK_HITSLOP} 72 + style={{width: 30, height: 30, marginTop: isWeb ? 6 : 4}} 73 + accessibilityRole="button" 74 + accessibilityLabel={_(msg`Back`)} 75 + accessibilityHint=""> 76 + <FontAwesomeIcon 77 + size={18} 78 + icon="angle-left" 79 + style={{ 80 + marginTop: 6, 81 + }} 82 + color={t.atoms.text.color} 83 + /> 84 + </TouchableOpacity> 87 85 88 86 {profile && moderation && blockInfo ? ( 89 87 <HeaderReady
+1
src/components/forms/DateField/index.android.tsx
··· 57 57 open 58 58 timeZoneOffsetInMinutes={0} 59 59 theme={t.scheme} 60 + // @ts-ignore TODO 60 61 buttonColor={t.name === 'light' ? '#000000' : '#ffffff'} 61 62 date={new Date(value)} 62 63 onConfirm={onChangeInternal}
+5
src/components/icons/FloppyDisk.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const FloppyDisk_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + 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', 5 + })
-31
src/lib/hooks/useWebBodyScrollLock.ts
··· 1 - import {useEffect} from 'react' 2 - 3 - import {isWeb} from '#/platform/detection' 4 - 5 - let refCount = 0 6 - 7 - function incrementRefCount() { 8 - if (refCount === 0) { 9 - document.body.style.overflow = 'hidden' 10 - document.documentElement.style.scrollbarGutter = 'auto' 11 - } 12 - refCount++ 13 - } 14 - 15 - function decrementRefCount() { 16 - refCount-- 17 - if (refCount === 0) { 18 - document.body.style.overflow = '' 19 - document.documentElement.style.scrollbarGutter = '' 20 - } 21 - } 22 - 23 - export function useWebBodyScrollLock(isLockActive: boolean) { 24 - useEffect(() => { 25 - if (!isWeb || !isLockActive) { 26 - return 27 - } 28 - incrementRefCount() 29 - return () => decrementRefCount() 30 - }) 31 - }
+7 -14
src/screens/Deactivated.tsx
··· 17 17 } from '#/state/session' 18 18 import {useSetMinimalShellMode} from '#/state/shell' 19 19 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 20 - import {ScrollView} from '#/view/com/util/Views' 21 20 import {Logo} from '#/view/icons/Logo' 22 21 import {atoms as a, useTheme} from '#/alf' 23 22 import {AccountList} from '#/components/AccountList' 24 23 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 25 24 import {Divider} from '#/components/Divider' 26 25 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 26 + import * as Layout from '#/components/Layout' 27 27 import {Loader} from '#/components/Loader' 28 28 import {Text} from '#/components/Typography' 29 29 ··· 104 104 }, [_, agent, setPending, setError, queryClient]) 105 105 106 106 return ( 107 - <View style={[a.util_screen_outer, a.flex_1, t.atoms.bg]}> 108 - <ScrollView 109 - style={[ 110 - a.h_full, 111 - a.w_full, 107 + <View style={[a.util_screen_outer, a.flex_1]}> 108 + <Layout.Content 109 + contentContainerStyle={[ 112 110 a.px_2xl, 113 111 { 114 112 paddingTop: isWeb ? 64 : insets.top + 16, 115 113 paddingBottom: isWeb ? 64 : insets.bottom, 116 114 }, 117 - ]} 118 - contentContainerStyle={[ 119 - a.w_full, 120 - a.flex_row, 121 - a.justify_center, 122 - {borderWidth: 0}, 123 115 ]}> 124 - <View style={[a.w_full, {maxWidth: COL_WIDTH}]}> 116 + <View 117 + style={[a.w_full, {marginHorizontal: 'auto', maxWidth: COL_WIDTH}]}> 125 118 <View style={[a.w_full, a.justify_center, a.align_center, a.pb_5xl]}> 126 119 <Logo width={40} /> 127 120 </View> ··· 218 211 </> 219 212 )} 220 213 </View> 221 - </ScrollView> 214 + </Layout.Content> 222 215 </View> 223 216 ) 224 217 }
+30 -41
src/screens/Hashtag.tsx
··· 1 1 import React from 'react' 2 - import {ListRenderItemInfo, Pressable, View} from 'react-native' 2 + import {ListRenderItemInfo, View} from 'react-native' 3 3 import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' 4 4 import {msg} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' ··· 13 13 import {cleanError} from '#/lib/strings/errors' 14 14 import {sanitizeHandle} from '#/lib/strings/handles' 15 15 import {enforceLen} from '#/lib/strings/helpers' 16 - import {isNative, isWeb} from '#/platform/detection' 17 16 import {useSearchPostsQuery} from '#/state/queries/search-posts' 18 17 import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell' 19 18 import {Pager} from '#/view/com/pager/Pager' 20 19 import {TabBar} from '#/view/com/pager/TabBar' 21 20 import {Post} from '#/view/com/post/Post' 22 21 import {List} from '#/view/com/util/List' 23 - import {ViewHeader} from '#/view/com/util/ViewHeader' 24 - import {CenteredView} from '#/view/com/util/Views' 25 - import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox' 22 + import {atoms as a, web} from '#/alf' 23 + import {Button, ButtonIcon} from '#/components/Button' 24 + import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' 26 25 import * as Layout from '#/components/Layout' 27 26 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 28 27 ··· 110 109 111 110 return ( 112 111 <Layout.Screen> 113 - <CenteredView sideBorders={true}> 114 - <ViewHeader 115 - showOnDesktop 116 - title={headerTitle} 117 - subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined} 118 - canGoBack 119 - renderButton={ 120 - isNative 121 - ? () => ( 122 - <Pressable 123 - accessibilityRole="button" 124 - onPress={onShare} 125 - hitSlop={HITSLOP_10}> 126 - <ArrowOutOfBox_Stroke2_Corner0_Rounded 127 - size="lg" 128 - onPress={onShare} 129 - /> 130 - </Pressable> 131 - ) 132 - : undefined 133 - } 134 - /> 135 - </CenteredView> 112 + <Layout.Header.Outer> 113 + <Layout.Header.BackButton /> 114 + <Layout.Header.Content> 115 + <Layout.Header.TitleText>{headerTitle}</Layout.Header.TitleText> 116 + {author && ( 117 + <Layout.Header.SubtitleText> 118 + {_(msg`From @${sanitizedAuthor}`)} 119 + </Layout.Header.SubtitleText> 120 + )} 121 + </Layout.Header.Content> 122 + <Layout.Header.Slot> 123 + <Button 124 + label={_(msg`Share`)} 125 + size="small" 126 + variant="ghost" 127 + color="primary" 128 + shape="round" 129 + onPress={onShare} 130 + hitSlop={HITSLOP_10} 131 + style={[{right: -3}]}> 132 + <ButtonIcon icon={Share} size="md" /> 133 + </Button> 134 + </Layout.Header.Slot> 135 + </Layout.Header.Outer> 136 136 <Pager 137 137 onPageSelected={onPageSelected} 138 138 renderTabBar={props => ( 139 - <CenteredView 140 - sideBorders={true} 141 - // @ts-ignore web only 142 - style={ 143 - isWeb 144 - ? { 145 - position: isWeb ? 'sticky' : '', 146 - top: 0, 147 - zIndex: 1, 148 - } 149 - : undefined 150 - }> 139 + <Layout.Center style={web([a.sticky, a.z_10, {top: 0}])}> 151 140 <TabBar items={sections.map(section => section.title)} {...props} /> 152 - </CenteredView> 141 + </Layout.Center> 153 142 )} 154 143 initialPage={0}> 155 144 {sections.map((section, i) => (
+55 -105
src/screens/Messages/ChatList.tsx
··· 16 16 import {useMessagesEventBus} from '#/state/messages/events' 17 17 import {useListConvosQuery} from '#/state/queries/messages/list-converations' 18 18 import {List} from '#/view/com/util/List' 19 - import {ViewHeader} from '#/view/com/util/ViewHeader' 20 - import {CenteredView} from '#/view/com/util/Views' 21 19 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 22 20 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 23 21 import {DialogControlProps, useDialogControl} from '#/components/Dialog' ··· 49 47 const {_} = useLingui() 50 48 const t = useTheme() 51 49 const newChatControl = useDialogControl() 52 - const {gtMobile} = useBreakpoints() 53 50 const pushToConversation = route.params?.pushToConversation 54 51 55 52 // Whenever we have `pushToConversation` set, it means we pressed a notification for a chat without being on ··· 81 78 }, [messagesBus, isActive]), 82 79 ) 83 80 84 - const renderButton = useCallback(() => { 85 - return ( 86 - <Link 87 - to="/messages/settings" 88 - label={_(msg`Chat settings`)} 89 - size="small" 90 - variant="ghost" 91 - color="secondary" 92 - shape="square" 93 - style={[a.justify_center]}> 94 - <SettingsSlider size="md" style={[t.atoms.text_contrast_medium]} /> 95 - </Link> 96 - ) 97 - }, [_, t]) 98 - 99 81 const initialNumToRender = useInitialNumToRender({minItemHeight: 80}) 100 82 const [isPTRing, setIsPTRing] = useState(false) 101 83 ··· 144 126 [navigation], 145 127 ) 146 128 147 - const onNavigateToSettings = useCallback(() => { 148 - navigation.navigate('MessagesSettings') 149 - }, [navigation]) 150 - 151 129 if (conversations.length < 1) { 152 130 return ( 153 131 <Layout.Screen> 154 - <CenteredView sideBorders={gtMobile} style={[a.h_full_vh]}> 155 - {gtMobile ? ( 156 - <DesktopHeader 157 - newChatControl={newChatControl} 158 - onNavigateToSettings={onNavigateToSettings} 159 - /> 160 - ) : ( 161 - <ViewHeader 162 - title={_(msg`Messages`)} 163 - renderButton={renderButton} 164 - showBorder 165 - canGoBack={false} 166 - /> 167 - )} 168 - 132 + <Header newChatControl={newChatControl} /> 133 + <Layout.Center> 169 134 {isLoading ? ( 170 135 <View style={[a.align_center, a.pt_3xl, web({paddingTop: '10vh'})]}> 171 136 <Loader size="xl" /> ··· 227 192 )} 228 193 </> 229 194 )} 230 - </CenteredView> 195 + </Layout.Center> 231 196 232 197 {!isLoading && !isError && ( 233 198 <NewChat onNewChat={onNewChat} control={newChatControl} /> ··· 238 203 239 204 return ( 240 205 <Layout.Screen testID="messagesScreen"> 241 - {!gtMobile && ( 242 - <ViewHeader 243 - title={_(msg`Messages`)} 244 - renderButton={renderButton} 245 - showBorder 246 - canGoBack={false} 247 - /> 248 - )} 206 + <Header newChatControl={newChatControl} /> 249 207 <NewChat onNewChat={onNewChat} control={newChatControl} /> 250 208 <List 251 209 data={conversations} ··· 254 212 refreshing={isPTRing} 255 213 onRefresh={onRefresh} 256 214 onEndReached={onEndReached} 257 - ListHeaderComponent={ 258 - <DesktopHeader 259 - newChatControl={newChatControl} 260 - onNavigateToSettings={onNavigateToSettings} 261 - /> 262 - } 263 215 ListFooterComponent={ 264 216 <ListFooter 265 217 isFetchingNextPage={isFetchingNextPage} ··· 276 228 windowSize={11} 277 229 // @ts-ignore our .web version only -sfn 278 230 desktopFixedHeight 231 + sideBorders={false} 279 232 /> 280 233 </Layout.Screen> 281 234 ) 282 235 } 283 236 284 - function DesktopHeader({ 285 - newChatControl, 286 - onNavigateToSettings, 287 - }: { 288 - newChatControl: DialogControlProps 289 - onNavigateToSettings: () => void 290 - }) { 291 - const t = useTheme() 237 + function Header({newChatControl}: {newChatControl: DialogControlProps}) { 292 238 const {_} = useLingui() 293 - const {gtMobile, gtTablet} = useBreakpoints() 239 + const {gtMobile} = useBreakpoints() 294 240 295 - if (!gtMobile) { 296 - return null 297 - } 241 + const settingsLink = ( 242 + <Link 243 + to="/messages/settings" 244 + label={_(msg`Chat settings`)} 245 + size="small" 246 + variant="ghost" 247 + color="secondary" 248 + shape="square" 249 + style={[a.justify_center]}> 250 + <ButtonIcon icon={SettingsSlider} size="md" /> 251 + </Link> 252 + ) 298 253 299 254 return ( 300 - <View 301 - style={[ 302 - t.atoms.bg, 303 - a.flex_row, 304 - a.align_center, 305 - a.justify_between, 306 - a.gap_lg, 307 - a.px_lg, 308 - a.pr_md, 309 - a.py_sm, 310 - a.border_b, 311 - t.atoms.border_contrast_low, 312 - ]}> 313 - <Text style={[a.text_2xl, a.font_bold]}> 314 - <Trans>Messages</Trans> 315 - </Text> 316 - <View style={[a.flex_row, a.align_center, a.gap_sm]}> 317 - <Button 318 - label={_(msg`Message settings`)} 319 - color="secondary" 320 - size="small" 321 - variant="ghost" 322 - shape="square" 323 - onPress={onNavigateToSettings}> 324 - <SettingsSlider size="md" style={[t.atoms.text_contrast_medium]} /> 325 - </Button> 326 - {gtTablet && ( 327 - <Button 328 - label={_(msg`New chat`)} 329 - color="primary" 330 - size="small" 331 - variant="solid" 332 - onPress={newChatControl.open}> 333 - <ButtonIcon icon={Plus} position="left" /> 334 - <ButtonText> 335 - <Trans>New chat</Trans> 336 - </ButtonText> 337 - </Button> 338 - )} 339 - </View> 340 - </View> 255 + <Layout.Header.Outer> 256 + {gtMobile ? ( 257 + <> 258 + <Layout.Header.Content> 259 + <Layout.Header.TitleText> 260 + <Trans>Messages</Trans> 261 + </Layout.Header.TitleText> 262 + </Layout.Header.Content> 263 + 264 + <View style={[a.flex_row, a.align_center, a.gap_sm]}> 265 + {settingsLink} 266 + <Button 267 + label={_(msg`New chat`)} 268 + color="primary" 269 + size="small" 270 + variant="solid" 271 + onPress={newChatControl.open}> 272 + <ButtonIcon icon={Plus} position="left" /> 273 + <ButtonText> 274 + <Trans>New chat</Trans> 275 + </ButtonText> 276 + </Button> 277 + </View> 278 + </> 279 + ) : ( 280 + <> 281 + <Layout.Header.MenuButton /> 282 + <Layout.Header.Content> 283 + <Layout.Header.TitleText> 284 + <Trans>Messages</Trans> 285 + </Layout.Header.TitleText> 286 + </Layout.Header.Content> 287 + <Layout.Header.Slot>{settingsLink}</Layout.Header.Slot> 288 + </> 289 + )} 290 + </Layout.Header.Outer> 341 291 ) 342 292 }
+4 -5
src/screens/Messages/Conversation.tsx
··· 17 17 import {useModerationOpts} from '#/state/preferences/moderation-opts' 18 18 import {useProfileQuery} from '#/state/queries/profile' 19 19 import {useSetMinimalShellMode} from '#/state/shell' 20 - import {CenteredView} from '#/view/com/util/Views' 21 20 import {MessagesList} from '#/screens/Messages/components/MessagesList' 22 21 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 23 22 import {useDialogControl} from '#/components/Dialog' ··· 97 96 98 97 if (convoState.status === ConvoStatus.Error) { 99 98 return ( 100 - <CenteredView style={[a.flex_1]} sideBorders> 99 + <Layout.Center style={[a.flex_1]}> 101 100 <MessagesListHeader /> 102 101 <Error 103 102 title={_(msg`Something went wrong`)} ··· 105 104 onRetry={() => convoState.error.retry()} 106 105 sideBorders={false} 107 106 /> 108 - </CenteredView> 107 + </Layout.Center> 109 108 ) 110 109 } 111 110 112 111 return ( 113 - <CenteredView style={[a.flex_1]} sideBorders> 112 + <Layout.Center style={[a.flex_1]}> 114 113 {!readyToShow && <MessagesListHeader />} 115 114 <View style={[a.flex_1]}> 116 115 {moderationOpts && recipient ? ( ··· 140 139 </View> 141 140 )} 142 141 </View> 143 - </CenteredView> 142 + </Layout.Center> 144 143 ) 145 144 } 146 145
+11 -5
src/screens/Messages/Settings.tsx
··· 10 10 import {useProfileQuery} from '#/state/queries/profile' 11 11 import {useSession} from '#/state/session' 12 12 import * as Toast from '#/view/com/util/Toast' 13 - import {ViewHeader} from '#/view/com/util/ViewHeader' 14 - import {ScrollView} from '#/view/com/util/Views' 15 13 import {atoms as a} from '#/alf' 16 14 import {Admonition} from '#/components/Admonition' 17 15 import {Divider} from '#/components/Divider' ··· 57 55 58 56 return ( 59 57 <Layout.Screen testID="messagesSettingsScreen"> 60 - <ScrollView stickyHeaderIndices={[0]}> 61 - <ViewHeader title={_(msg`Chat Settings`)} showOnDesktop showBorder /> 58 + <Layout.Header.Outer> 59 + <Layout.Header.BackButton /> 60 + <Layout.Header.Content> 61 + <Layout.Header.TitleText> 62 + <Trans>Chat Settings</Trans> 63 + </Layout.Header.TitleText> 64 + </Layout.Header.Content> 65 + <Layout.Header.Slot /> 66 + </Layout.Header.Outer> 67 + <Layout.Content> 62 68 <View style={[a.p_lg, a.gap_md]}> 63 69 <Text style={[a.text_lg, a.font_bold]}> 64 70 <Trans>Allow new messages from</Trans> ··· 142 148 </> 143 149 )} 144 150 </View> 145 - </ScrollView> 151 + </Layout.Content> 146 152 </Layout.Screen> 147 153 ) 148 154 }
+13 -34
src/screens/Moderation/index.tsx
··· 1 - import React from 'react' 1 + import {Fragment, useCallback} from 'react' 2 2 import {Linking, View} from 'react-native' 3 - import {useSafeAreaFrame} from 'react-native-safe-area-context' 4 3 import {LABELS} from '@atproto/api' 5 4 import {msg, Trans} from '@lingui/macro' 6 5 import {useLingui} from '@lingui/react' ··· 19 18 import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities' 20 19 import {useSetMinimalShellMode} from '#/state/shell' 21 20 import {ViewHeader} from '#/view/com/util/ViewHeader' 22 - import {CenteredView} from '#/view/com/util/Views' 23 - import {ScrollView} from '#/view/com/util/Views' 24 21 import {atoms as a, useBreakpoints, useTheme, ViewStyleProp} from '#/alf' 25 22 import {Button, ButtonText} from '#/components/Button' 26 23 import * as Dialog from '#/components/Dialog' ··· 37 34 import * as LabelingService from '#/components/LabelingServiceCard' 38 35 import * as Layout from '#/components/Layout' 39 36 import {InlineLinkText, Link} from '#/components/Link' 37 + import {ListMaybePlaceholder} from '#/components/Lists' 40 38 import {Loader} from '#/components/Loader' 41 39 import {GlobalLabelPreference} from '#/components/moderation/LabelPreference' 42 40 import {Text} from '#/components/Typography' ··· 75 73 export function ModerationScreen( 76 74 _props: NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>, 77 75 ) { 78 - const t = useTheme() 79 76 const {_} = useLingui() 80 77 const { 81 78 isLoading: isPreferencesLoading, 82 79 error: preferencesError, 83 80 data: preferences, 84 81 } = usePreferencesQuery() 85 - const {gtMobile} = useBreakpoints() 86 - const {height} = useSafeAreaFrame() 87 82 88 83 const isLoading = isPreferencesLoading 89 84 const error = preferencesError 90 85 91 86 return ( 92 87 <Layout.Screen testID="moderationScreen"> 93 - <CenteredView 94 - testID="moderationScreen" 95 - style={[ 96 - t.atoms.border_contrast_low, 97 - t.atoms.bg, 98 - {minHeight: height}, 99 - ...(gtMobile ? [a.border_l, a.border_r] : []), 100 - ]}> 101 - <ViewHeader title={_(msg`Moderation`)} showOnDesktop /> 102 - 88 + <ViewHeader title={_(msg`Moderation`)} showOnDesktop /> 89 + <Layout.Content> 103 90 {isLoading ? ( 104 - <View style={[a.w_full, a.align_center, a.pt_2xl]}> 105 - <Loader size="xl" fill={t.atoms.text.color} /> 106 - </View> 91 + <ListMaybePlaceholder isLoading={true} sideBorders={false} /> 107 92 ) : error || !preferences ? ( 108 93 <ErrorState 109 94 error={ ··· 114 99 ) : ( 115 100 <ModerationScreenInner preferences={preferences} /> 116 101 )} 117 - </CenteredView> 102 + </Layout.Content> 118 103 </Layout.Screen> 119 104 ) 120 105 } ··· 169 154 } = useMyLabelersQuery() 170 155 171 156 useFocusEffect( 172 - React.useCallback(() => { 157 + useCallback(() => { 173 158 setMinimalShellMode(false) 174 159 }, [setMinimalShellMode]), 175 160 ) ··· 183 168 const ageNotSet = !preferences.userAge 184 169 const isUnderage = (preferences.userAge || 0) < 18 185 170 186 - const onToggleAdultContentEnabled = React.useCallback( 171 + const onToggleAdultContentEnabled = useCallback( 187 172 async (selected: boolean) => { 188 173 try { 189 174 await setAdultContentPref({ ··· 201 186 const disabledOnIOS = isIOS && !adultContentEnabled 202 187 203 188 return ( 204 - <ScrollView 205 - contentContainerStyle={[ 206 - a.border_0, 207 - a.pt_2xl, 208 - a.px_lg, 209 - gtMobile && a.px_2xl, 210 - ]}> 189 + <View style={[a.pt_2xl, a.px_lg, gtMobile && a.px_2xl]}> 211 190 <Text 212 191 style={[a.text_md, a.font_bold, a.pb_md, t.atoms.text_contrast_high]}> 213 192 <Trans>Moderation tools</Trans> ··· 420 399 <View style={[a.rounded_sm, t.atoms.bg_contrast_25]}> 421 400 {labelers.map((labeler, i) => { 422 401 return ( 423 - <React.Fragment key={labeler.creator.did}> 402 + <Fragment key={labeler.creator.did}> 424 403 {i !== 0 && <Divider />} 425 404 <LabelingService.Link labeler={labeler}> 426 405 {state => ( ··· 457 436 </LabelingService.Outer> 458 437 )} 459 438 </LabelingService.Link> 460 - </React.Fragment> 439 + </Fragment> 461 440 ) 462 441 })} 463 442 </View> 464 443 )} 465 - <View style={{height: 200}} /> 466 - </ScrollView> 444 + <View style={{height: 150}} /> 445 + </View> 467 446 ) 468 447 }
+2 -4
src/screens/Onboarding/Layout.tsx
··· 1 1 import React from 'react' 2 - import {View} from 'react-native' 3 - import Animated from 'react-native-reanimated' 2 + import {ScrollView, View} from 'react-native' 4 3 import {useSafeAreaInsets} from 'react-native-safe-area-context' 5 4 import {msg} from '@lingui/macro' 6 5 import {useLingui} from '@lingui/react' 7 6 8 7 import {isWeb} from '#/platform/detection' 9 8 import {useOnboardingDispatch} from '#/state/shell' 10 - import {ScrollView} from '#/view/com/util/Views' 11 9 import {Context} from '#/screens/Onboarding/state' 12 10 import { 13 11 atoms as a, ··· 36 34 const {gtMobile} = useBreakpoints() 37 35 const onboardDispatch = useOnboardingDispatch() 38 36 const {state, dispatch} = React.useContext(Context) 39 - const scrollview = React.useRef<Animated.ScrollView>(null) 37 + const scrollview = React.useRef<ScrollView>(null) 40 38 const prevActiveStep = React.useRef<string>(state.activeStep) 41 39 42 40 React.useEffect(() => {
+2 -8
src/screens/Post/PostLikedBy.tsx
··· 5 5 6 6 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 7 7 import {makeRecordUri} from '#/lib/strings/url-helpers' 8 - import {isWeb} from '#/platform/detection' 9 8 import {useSetMinimalShellMode} from '#/state/shell' 10 9 import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy' 11 10 import {ViewHeader} from '#/view/com/util/ViewHeader' 12 - import {CenteredView} from '#/view/com/util/Views' 13 11 import * as Layout from '#/components/Layout' 14 - import {ListHeaderDesktop} from '#/components/Lists' 15 12 16 13 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'> 17 14 export const PostLikedByScreen = ({route}: Props) => { ··· 28 25 29 26 return ( 30 27 <Layout.Screen> 31 - <CenteredView sideBorders={true}> 32 - <ListHeaderDesktop title={_(msg`Liked By`)} /> 33 - <ViewHeader title={_(msg`Liked By`)} showBorder={!isWeb} /> 34 - <PostLikedByComponent uri={uri} /> 35 - </CenteredView> 28 + <ViewHeader title={_(msg`Liked By`)} /> 29 + <PostLikedByComponent uri={uri} /> 36 30 </Layout.Screen> 37 31 ) 38 32 }
-2
src/screens/Post/PostQuotes.tsx
··· 11 11 import {ViewHeader} from '#/view/com/util/ViewHeader' 12 12 import {CenteredView} from '#/view/com/util/Views' 13 13 import * as Layout from '#/components/Layout' 14 - import {ListHeaderDesktop} from '#/components/Lists' 15 14 16 15 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostQuotes'> 17 16 export const PostQuotesScreen = ({route}: Props) => { ··· 29 28 return ( 30 29 <Layout.Screen> 31 30 <CenteredView sideBorders={true}> 32 - <ListHeaderDesktop title={_(msg`Quotes`)} /> 33 31 <ViewHeader title={_(msg`Quotes`)} showBorder={!isWeb} /> 34 32 <PostQuotesComponent uri={uri} /> 35 33 </CenteredView>
-2
src/screens/Post/PostRepostedBy.tsx
··· 11 11 import {ViewHeader} from '#/view/com/util/ViewHeader' 12 12 import {CenteredView} from '#/view/com/util/Views' 13 13 import * as Layout from '#/components/Layout' 14 - import {ListHeaderDesktop} from '#/components/Lists' 15 14 16 15 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'> 17 16 export const PostRepostedByScreen = ({route}: Props) => { ··· 29 28 return ( 30 29 <Layout.Screen> 31 30 <CenteredView sideBorders={true}> 32 - <ListHeaderDesktop title={_(msg`Reposted By`)} /> 33 31 <ViewHeader title={_(msg`Reposted By`)} showBorder={!isWeb} /> 34 32 <PostRepostedByComponent uri={uri} /> 35 33 </CenteredView>
+19 -10
src/screens/Profile/KnownFollowers.tsx
··· 15 15 import {List} from '#/view/com/util/List' 16 16 import {ViewHeader} from '#/view/com/util/ViewHeader' 17 17 import * as Layout from '#/components/Layout' 18 - import { 19 - ListFooter, 20 - ListHeaderDesktop, 21 - ListMaybePlaceholder, 22 - } from '#/components/Lists' 18 + import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 23 19 24 - function renderItem({item}: {item: AppBskyActorDefs.ProfileViewBasic}) { 25 - return <ProfileCardWithFollowBtn key={item.did} profile={item} /> 20 + function renderItem({ 21 + item, 22 + index, 23 + }: { 24 + item: AppBskyActorDefs.ProfileViewBasic 25 + index: number 26 + }) { 27 + return ( 28 + <ProfileCardWithFollowBtn 29 + key={item.did} 30 + profile={item} 31 + noBorder={index === 0} 32 + /> 33 + ) 26 34 } 27 35 28 36 function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) { ··· 93 101 if (followers.length < 1) { 94 102 return ( 95 103 <Layout.Screen> 104 + <ViewHeader title={_(msg`Followers you know`)} /> 96 105 <ListMaybePlaceholder 97 106 isLoading={isDidLoading || isFollowersLoading} 98 107 isError={isError} ··· 100 109 emptyMessage={_(msg`You don't follow any users who follow @${name}.`)} 101 110 errorMessage={cleanError(resolveError || error)} 102 111 onRetry={isError ? refetch : undefined} 112 + topBorder={false} 113 + sideBorders={false} 103 114 /> 104 115 </Layout.Screen> 105 116 ) ··· 116 127 onRefresh={onRefresh} 117 128 onEndReached={onEndReached} 118 129 onEndReachedThreshold={4} 119 - ListHeaderComponent={ 120 - <ListHeaderDesktop title={_(msg`Followers you know`)} /> 121 - } 122 130 ListFooterComponent={ 123 131 <ListFooter 124 132 isFetchingNextPage={isFetchingNextPage} ··· 130 138 desktopFixedHeight 131 139 initialNumToRender={initialNumToRender} 132 140 windowSize={11} 141 + sideBorders={false} 133 142 /> 134 143 </Layout.Screen> 135 144 )
+4 -3
src/screens/Profile/Sections/Labels.tsx
··· 15 15 import {useScrollHandlers} from '#/lib/ScrollContext' 16 16 import {isNative} from '#/platform/detection' 17 17 import {ListRef} from '#/view/com/util/List' 18 - import {CenteredView, ScrollView} from '#/view/com/util/Views' 18 + import {ScrollView} from '#/view/com/util/Views' 19 19 import {atoms as a, useTheme} from '#/alf' 20 20 import {Divider} from '#/components/Divider' 21 21 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 22 + import * as Layout from '#/components/Layout' 22 23 import {Loader} from '#/components/Loader' 23 24 import {LabelerLabelPreference} from '#/components/moderation/LabelPreference' 24 25 import {Text} from '#/components/Typography' ··· 75 76 }, [isFocused, scrollElRef, setScrollViewTag]) 76 77 77 78 return ( 78 - <CenteredView style={{flex: 1, minHeight}} sideBorders> 79 + <Layout.Center style={{flex: 1, minHeight}}> 79 80 {isLabelerLoading ? ( 80 81 <View style={[a.w_full, a.align_center]}> 81 82 <Loader size="xl" /> ··· 95 96 headerHeight={headerHeight} 96 97 /> 97 98 )} 98 - </CenteredView> 99 + </Layout.Center> 99 100 ) 100 101 }) 101 102
+9 -1
src/screens/Settings/AboutSettings.tsx
··· 21 21 22 22 return ( 23 23 <Layout.Screen> 24 - <Layout.Header title={_(msg`About`)} /> 24 + <Layout.Header.Outer> 25 + <Layout.Header.BackButton /> 26 + <Layout.Header.Content> 27 + <Layout.Header.TitleText> 28 + <Trans>About</Trans> 29 + </Layout.Header.TitleText> 30 + </Layout.Header.Content> 31 + <Layout.Header.Slot /> 32 + </Layout.Header.Outer> 25 33 <Layout.Content> 26 34 <SettingsList.Container> 27 35 <SettingsList.LinkItem
+9 -1
src/screens/Settings/AccessibilitySettings.tsx
··· 39 39 40 40 return ( 41 41 <Layout.Screen> 42 - <Layout.Header title={_(msg`Accessibility`)} /> 42 + <Layout.Header.Outer> 43 + <Layout.Header.BackButton /> 44 + <Layout.Header.Content> 45 + <Layout.Header.TitleText> 46 + <Trans>Accessibility</Trans> 47 + </Layout.Header.TitleText> 48 + </Layout.Header.Content> 49 + <Layout.Header.Slot /> 50 + </Layout.Header.Outer> 43 51 <Layout.Content> 44 52 <SettingsList.Container> 45 53 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
+9 -1
src/screens/Settings/AccountSettings.tsx
··· 38 38 39 39 return ( 40 40 <Layout.Screen> 41 - <Layout.Header title={_(msg`Account`)} /> 41 + <Layout.Header.Outer> 42 + <Layout.Header.BackButton /> 43 + <Layout.Header.Content> 44 + <Layout.Header.TitleText> 45 + <Trans>Account</Trans> 46 + </Layout.Header.TitleText> 47 + </Layout.Header.Content> 48 + <Layout.Header.Slot /> 49 + </Layout.Header.Outer> 42 50 <Layout.Content> 43 51 <SettingsList.Container> 44 52 <SettingsList.Item>
+10 -2
src/screens/Settings/AppIconSettings.tsx
··· 1 1 import React from 'react' 2 2 import {Alert, View} from 'react-native' 3 3 import {Image} from 'expo-image' 4 - import {msg} from '@lingui/macro' 4 + import {msg, Trans} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 6 import * as AppIcon from '@mozzius/expo-dynamic-app-icon' 7 7 import {NativeStackScreenProps} from '@react-navigation/native-stack' ··· 20 20 21 21 return ( 22 22 <Layout.Screen> 23 - <Layout.Header title={_('App Icon')} /> 23 + <Layout.Header.Outer> 24 + <Layout.Header.BackButton /> 25 + <Layout.Header.Content> 26 + <Layout.Header.TitleText> 27 + <Trans>App Icon</Trans> 28 + </Layout.Header.TitleText> 29 + </Layout.Header.Content> 30 + <Layout.Header.Slot /> 31 + </Layout.Header.Outer> 24 32 <Layout.Content 25 33 contentContainerStyle={[a.py_2xl, a.px_xl, {paddingBottom: 100}]}> 26 34 <Text style={[a.text_lg, a.font_heavy]}>Defaults</Text>
+9 -1
src/screens/Settings/AppPasswords.tsx
··· 44 44 45 45 return ( 46 46 <Layout.Screen testID="AppPasswordsScreen"> 47 - <Layout.Header title={_(msg`App Passwords`)} /> 47 + <Layout.Header.Outer> 48 + <Layout.Header.BackButton /> 49 + <Layout.Header.Content> 50 + <Layout.Header.TitleText> 51 + <Trans>App Passwords</Trans> 52 + </Layout.Header.TitleText> 53 + </Layout.Header.Content> 54 + <Layout.Header.Slot /> 55 + </Layout.Header.Outer> 48 56 <Layout.Content> 49 57 {error ? ( 50 58 <ErrorScreen
+9 -1
src/screens/Settings/AppearanceSettings.tsx
··· 79 79 return ( 80 80 <LayoutAnimationConfig skipExiting skipEntering> 81 81 <Layout.Screen testID="preferencesThreadsScreen"> 82 - <Layout.Header title={_(msg`Appearance`)} /> 82 + <Layout.Header.Outer> 83 + <Layout.Header.BackButton /> 84 + <Layout.Header.Content> 85 + <Layout.Header.TitleText> 86 + <Trans>Appearance</Trans> 87 + </Layout.Header.TitleText> 88 + </Layout.Header.Content> 89 + <Layout.Header.Slot /> 90 + </Layout.Header.Outer> 83 91 <Layout.Content> 84 92 <SettingsList.Container> 85 93 <AppearanceToggleButtonGroup
+9 -1
src/screens/Settings/ContentAndMediaSettings.tsx
··· 32 32 33 33 return ( 34 34 <Layout.Screen> 35 - <Layout.Header title={_(msg`Content and Media`)} /> 35 + <Layout.Header.Outer> 36 + <Layout.Header.BackButton /> 37 + <Layout.Header.Content> 38 + <Layout.Header.TitleText> 39 + <Trans>Content & Media</Trans> 40 + </Layout.Header.TitleText> 41 + </Layout.Header.Content> 42 + <Layout.Header.Slot /> 43 + </Layout.Header.Outer> 36 44 <Layout.Content> 37 45 <SettingsList.Container> 38 46 <SettingsList.LinkItem
+10 -4
src/screens/Settings/ExternalMediaPreferences.tsx
··· 1 1 import {Fragment} from 'react' 2 2 import {View} from 'react-native' 3 - import {msg, Trans} from '@lingui/macro' 4 - import {useLingui} from '@lingui/react' 3 + import {Trans} from '@lingui/macro' 5 4 6 5 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 7 6 import { ··· 23 22 'PreferencesExternalEmbeds' 24 23 > 25 24 export function ExternalMediaPreferencesScreen({}: Props) { 26 - const {_} = useLingui() 27 25 return ( 28 26 <Layout.Screen testID="externalMediaPreferencesScreen"> 29 - <Layout.Header title={_(msg`External Media Preferences`)} /> 27 + <Layout.Header.Outer> 28 + <Layout.Header.BackButton /> 29 + <Layout.Header.Content> 30 + <Layout.Header.TitleText> 31 + <Trans>External Media Preferences</Trans> 32 + </Layout.Header.TitleText> 33 + </Layout.Header.Content> 34 + <Layout.Header.Slot /> 35 + </Layout.Header.Outer> 30 36 <Layout.Content> 31 37 <SettingsList.Container> 32 38 <SettingsList.Item>
+9 -1
src/screens/Settings/FollowingFeedPreferences.tsx
··· 46 46 47 47 return ( 48 48 <Layout.Screen testID="followingFeedPreferencesScreen"> 49 - <Layout.Header title={_(msg`Following Feed Preferences`)} /> 49 + <Layout.Header.Outer> 50 + <Layout.Header.BackButton /> 51 + <Layout.Header.Content> 52 + <Layout.Header.TitleText> 53 + <Trans>Following Feed Preferences</Trans> 54 + </Layout.Header.TitleText> 55 + </Layout.Header.Content> 56 + <Layout.Header.Slot /> 57 + </Layout.Header.Outer> 50 58 <Layout.Content> 51 59 <SettingsList.Container> 52 60 <SettingsList.Item>
+9 -1
src/screens/Settings/LanguageSettings.tsx
··· 64 64 65 65 return ( 66 66 <Layout.Screen testID="PreferencesLanguagesScreen"> 67 - <Layout.Header title={_(msg`Languages`)} /> 67 + <Layout.Header.Outer> 68 + <Layout.Header.BackButton /> 69 + <Layout.Header.Content> 70 + <Layout.Header.TitleText> 71 + <Trans>Languages</Trans> 72 + </Layout.Header.TitleText> 73 + </Layout.Header.Content> 74 + <Layout.Header.Slot /> 75 + </Layout.Header.Outer> 68 76 <Layout.Content> 69 77 <SettingsList.Container> 70 78 <SettingsList.Group iconInset={false}>
+9 -1
src/screens/Settings/NotificationSettings.tsx
··· 33 33 34 34 return ( 35 35 <Layout.Screen> 36 - <Layout.Header title={_(msg`Notification Settings`)} /> 36 + <Layout.Header.Outer> 37 + <Layout.Header.BackButton /> 38 + <Layout.Header.Content> 39 + <Layout.Header.TitleText> 40 + <Trans>Notification Settings</Trans> 41 + </Layout.Header.TitleText> 42 + </Layout.Header.Content> 43 + <Layout.Header.Slot /> 44 + </Layout.Header.Outer> 37 45 <Layout.Content> 38 46 {isQueryError ? ( 39 47 <Error
+9 -1
src/screens/Settings/PrivacyAndSecuritySettings.tsx
··· 29 29 30 30 return ( 31 31 <Layout.Screen> 32 - <Layout.Header title={_(msg`Privacy and Security`)} /> 32 + <Layout.Header.Outer> 33 + <Layout.Header.BackButton /> 34 + <Layout.Header.Content> 35 + <Layout.Header.TitleText> 36 + <Trans>Privacy and Security</Trans> 37 + </Layout.Header.TitleText> 38 + </Layout.Header.Content> 39 + <Layout.Header.Slot /> 40 + </Layout.Header.Outer> 33 41 <Layout.Content> 34 42 <SettingsList.Container> 35 43 <SettingsList.Item>
+9 -1
src/screens/Settings/Settings.tsx
··· 73 73 74 74 return ( 75 75 <Layout.Screen> 76 - <Layout.Header title={_(msg`Settings`)} /> 76 + <Layout.Header.Outer> 77 + <Layout.Header.BackButton /> 78 + <Layout.Header.Content> 79 + <Layout.Header.TitleText> 80 + <Trans>Settings</Trans> 81 + </Layout.Header.TitleText> 82 + </Layout.Header.Content> 83 + <Layout.Header.Slot /> 84 + </Layout.Header.Outer> 77 85 <Layout.Content> 78 86 <SettingsList.Container> 79 87 <View
+9 -1
src/screens/Settings/ThreadPreferences.tsx
··· 38 38 39 39 return ( 40 40 <Layout.Screen testID="threadPreferencesScreen"> 41 - <Layout.Header title={_(msg`Thread Preferences`)} /> 41 + <Layout.Header.Outer> 42 + <Layout.Header.BackButton /> 43 + <Layout.Header.Content> 44 + <Layout.Header.TitleText> 45 + <Trans>Thread Preferences</Trans> 46 + </Layout.Header.TitleText> 47 + </Layout.Header.Content> 48 + <Layout.Header.Slot /> 49 + </Layout.Header.Outer> 42 50 <Layout.Content> 43 51 <SettingsList.Container> 44 52 <SettingsList.Group>
+1 -2
src/screens/SignupQueued.tsx
··· 1 1 import React from 'react' 2 - import {Modal, View} from 'react-native' 2 + import {Modal, ScrollView, View} from 'react-native' 3 3 import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 4 import {StatusBar} from 'expo-status-bar' 5 5 import {msg, plural, Trans} from '@lingui/macro' ··· 9 9 import {isIOS, isWeb} from '#/platform/detection' 10 10 import {isSignupQueued, useAgent, useSessionApi} from '#/state/session' 11 11 import {useOnboardingDispatch} from '#/state/shell' 12 - import {ScrollView} from '#/view/com/util/Views' 13 12 import {Logo} from '#/view/icons/Logo' 14 13 import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf' 15 14 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+29 -52
src/screens/StarterPack/Wizard/index.tsx
··· 1 1 import React from 'react' 2 - import {Keyboard, TouchableOpacity, View} from 'react-native' 2 + import {Keyboard, View} from 'react-native' 3 3 import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' 4 4 import {useSafeAreaInsets} from 'react-native-safe-area-context' 5 5 import {Image} from 'expo-image' ··· 10 10 ModerationOpts, 11 11 } from '@atproto/api' 12 12 import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' 13 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 14 13 import {msg, Plural, Trans} from '@lingui/macro' 15 14 import {useLingui} from '@lingui/react' 16 15 import {useFocusEffect, useNavigation} from '@react-navigation/native' 17 16 import {NativeStackScreenProps} from '@react-navigation/native-stack' 18 17 19 - import {HITSLOP_10, STARTER_PACK_MAX_SIZE} from '#/lib/constants' 18 + import {STARTER_PACK_MAX_SIZE} from '#/lib/constants' 20 19 import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' 21 20 import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' 22 21 import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' ··· 29 28 parseStarterPackUri, 30 29 } from '#/lib/strings/starter-pack' 31 30 import {logger} from '#/logger' 32 - import {isAndroid, isNative, isWeb} from '#/platform/detection' 31 + import {isNative} from '#/platform/detection' 33 32 import {useModerationOpts} from '#/state/preferences/moderation-opts' 34 33 import {useAllListMembersQuery} from '#/state/queries/list-members' 35 34 import {useProfileQuery} from '#/state/queries/profile' ··· 147 146 }) { 148 147 const navigation = useNavigation<NavigationProp>() 149 148 const {_} = useLingui() 150 - const t = useTheme() 151 149 const setMinimalShellMode = useSetMinimalShellMode() 152 150 const [state, dispatch] = useWizardState() 153 151 const {currentAccount} = useSession() ··· 283 281 284 282 return ( 285 283 <CenteredView style={[a.flex_1]} sideBorders> 286 - <View 287 - style={[ 288 - a.flex_row, 289 - a.pb_sm, 290 - a.px_md, 291 - a.border_b, 292 - t.atoms.border_contrast_medium, 293 - a.gap_sm, 294 - a.justify_between, 295 - a.align_center, 296 - isAndroid && a.pt_sm, 297 - isWeb && [a.py_md], 298 - ]}> 299 - <View style={[{width: 65}]}> 300 - <TouchableOpacity 301 - testID="viewHeaderDrawerBtn" 302 - hitSlop={HITSLOP_10} 303 - accessibilityRole="button" 304 - accessibilityLabel={_(msg`Back`)} 305 - accessibilityHint={_(msg`Go back to the previous step`)} 306 - onPress={() => { 307 - if (state.currentStep === 'Details') { 308 - navigation.pop() 309 - } else { 310 - dispatch({type: 'Back'}) 311 - } 312 - }}> 313 - <FontAwesomeIcon 314 - size={18} 315 - icon="angle-left" 316 - color={t.atoms.text.color} 317 - /> 318 - </TouchableOpacity> 319 - </View> 320 - <Text style={[a.flex_1, a.font_bold, a.text_lg, a.text_center]}> 321 - {currUiStrings.header} 322 - </Text> 323 - <View style={[{width: 65}]} /> 324 - </View> 284 + <Layout.Header.Outer> 285 + <Layout.Header.BackButton 286 + label={_(msg`Back`)} 287 + accessibilityHint={_(msg`Go back to the previous step`)} 288 + onPress={evt => { 289 + if (state.currentStep !== 'Details') { 290 + evt.preventDefault() 291 + dispatch({type: 'Back'}) 292 + } 293 + }} 294 + /> 295 + <Layout.Header.Content> 296 + <Layout.Header.TitleText> 297 + {currUiStrings.header} 298 + </Layout.Header.TitleText> 299 + </Layout.Header.Content> 300 + <Layout.Header.Slot /> 301 + </Layout.Header.Outer> 325 302 326 303 <Container> 327 304 {state.currentStep === 'Details' ? ( ··· 463 440 <Trans> 464 441 <Text style={[a.font_bold, textStyles]}>You</Text> and 465 442 <Text> </Text> 466 - <Text style={[a.font_bold, textStyles]}> 443 + <Text style={[a.font_bold, textStyles]} emoji> 467 444 {getName(items[1] /* [0] is self, skip it */)}{' '} 468 445 </Text> 469 446 are included in your starter pack 470 447 </Trans> 471 448 ) : items.length > 2 ? ( 472 449 <Trans context="profiles"> 473 - <Text style={[a.font_bold, textStyles]}> 450 + <Text style={[a.font_bold, textStyles]} emoji> 474 451 {getName(items[1] /* [0] is self, skip it */)},{' '} 475 452 </Text> 476 - <Text style={[a.font_bold, textStyles]}> 453 + <Text style={[a.font_bold, textStyles]} emoji> 477 454 {getName(items[2])},{' '} 478 455 </Text> 479 456 and{' '} ··· 504 481 { 505 482 items.length === 1 ? ( 506 483 <Trans> 507 - <Text style={[a.font_bold, textStyles]}> 484 + <Text style={[a.font_bold, textStyles]} emoji> 508 485 {getName(items[0])} 509 486 </Text>{' '} 510 487 is included in your starter pack 511 488 </Trans> 512 489 ) : items.length === 2 ? ( 513 490 <Trans> 514 - <Text style={[a.font_bold, textStyles]}> 491 + <Text style={[a.font_bold, textStyles]} emoji> 515 492 {getName(items[0])} 516 493 </Text>{' '} 517 494 and 518 495 <Text> </Text> 519 - <Text style={[a.font_bold, textStyles]}> 496 + <Text style={[a.font_bold, textStyles]} emoji> 520 497 {getName(items[1])}{' '} 521 498 </Text> 522 499 are included in your starter pack 523 500 </Trans> 524 501 ) : items.length > 2 ? ( 525 502 <Trans context="feeds"> 526 - <Text style={[a.font_bold, textStyles]}> 503 + <Text style={[a.font_bold, textStyles]} emoji> 527 504 {getName(items[0])},{' '} 528 505 </Text> 529 - <Text style={[a.font_bold, textStyles]}> 506 + <Text style={[a.font_bold, textStyles]} emoji> 530 507 {getName(items[1])},{' '} 531 508 </Text> 532 509 and{' '}
+1 -1
src/view/com/feeds/FeedPage.tsx
··· 108 108 }, [scrollToTop, feed, queryClient, setHasNew]) 109 109 110 110 return ( 111 - <View testID={testID} style={s.h100pct}> 111 + <View testID={testID}> 112 112 <MainScrollProvider> 113 113 <FeedFeedbackProvider value={feedFeedback}> 114 114 <Feed
+36 -90
src/view/com/home/HomeHeaderLayout.web.tsx
··· 1 1 import React from 'react' 2 - import {StyleSheet, View} from 'react-native' 2 + import {View} from 'react-native' 3 3 import Animated from 'react-native-reanimated' 4 4 import {msg} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 6 7 7 import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform' 8 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 9 8 import {useKawaiiMode} from '#/state/preferences/kawaii' 10 9 import {useSession} from '#/state/session' 11 10 import {useShellLayout} from '#/state/shell/shell-layout' 11 + import {HomeHeaderLayoutMobile} from '#/view/com/home/HomeHeaderLayoutMobile' 12 12 import {Logo} from '#/view/icons/Logo' 13 - import {atoms as a, useTheme} from '#/alf' 13 + import {atoms as a, useBreakpoints, useGutterStyles, useTheme} from '#/alf' 14 + import {ButtonIcon} from '#/components/Button' 14 15 import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag' 16 + import * as Layout from '#/components/Layout' 15 17 import {Link} from '#/components/Link' 16 - import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile' 17 18 18 19 export function HomeHeaderLayout(props: { 19 20 children: React.ReactNode 20 21 tabBarAnchor: JSX.Element | null | undefined 21 22 }) { 22 - const {isMobile} = useWebMediaQueries() 23 - if (isMobile) { 23 + const {gtMobile} = useBreakpoints() 24 + if (!gtMobile) { 24 25 return <HomeHeaderLayoutMobile {...props} /> 25 26 } else { 26 27 return <HomeHeaderLayoutDesktopAndTablet {...props} /> ··· 40 41 const {hasSession} = useSession() 41 42 const {_} = useLingui() 42 43 const kawaii = useKawaiiMode() 44 + const gutter = useGutterStyles() 43 45 44 46 return ( 45 47 <> 46 48 {hasSession && ( 47 - <View 48 - style={[ 49 - a.relative, 50 - a.flex_row, 51 - a.justify_end, 52 - a.align_center, 53 - a.pt_lg, 54 - a.px_md, 55 - a.pb_2xs, 56 - t.atoms.bg, 57 - t.atoms.border_contrast_low, 58 - styles.bar, 59 - kawaii && {paddingTop: 22, paddingBottom: 16}, 60 - ]}> 49 + <Layout.Center> 61 50 <View 62 - style={[ 63 - a.absolute, 64 - a.inset_0, 65 - a.pt_lg, 66 - a.m_auto, 67 - kawaii && {paddingTop: 4, paddingBottom: 0}, 68 - { 69 - width: kawaii ? 84 : 28, 70 - }, 71 - ]}> 72 - <Logo width={kawaii ? 60 : 28} /> 51 + style={[a.flex_row, a.align_center, a.pt_md, gutter, t.atoms.bg]}> 52 + <View style={{width: 34}} /> 53 + <View style={[a.flex_1, a.align_center, a.justify_center]}> 54 + <Logo width={kawaii ? 60 : 28} /> 55 + </View> 56 + <Link 57 + to="/feeds" 58 + hitSlop={10} 59 + label={_(msg`View your feeds and explore more`)} 60 + size="small" 61 + variant="ghost" 62 + color="secondary" 63 + shape="square" 64 + style={[a.justify_center]}> 65 + <ButtonIcon icon={FeedsIcon} size="lg" /> 66 + </Link> 73 67 </View> 74 - 75 - <Link 76 - to="/feeds" 77 - hitSlop={10} 78 - label={_(msg`View your feeds and explore more`)} 79 - size="small" 80 - variant="ghost" 81 - color="secondary" 82 - shape="square" 83 - style={[ 84 - a.justify_center, 85 - { 86 - marginTop: -4, 87 - }, 88 - ]}> 89 - <FeedsIcon size="md" fill={t.atoms.text_contrast_medium.color} /> 90 - </Link> 91 - </View> 68 + </Layout.Center> 92 69 )} 93 70 {tabBarAnchor} 94 - <Animated.View 95 - onLayout={e => { 96 - headerHeight.set(e.nativeEvent.layout.height) 97 - }} 98 - style={[ 99 - t.atoms.bg, 100 - t.atoms.border_contrast_low, 101 - styles.bar, 102 - styles.tabBar, 103 - headerMinimalShellTransform, 104 - ]}> 105 - {children} 106 - </Animated.View> 71 + <Layout.Center 72 + style={[a.sticky, a.z_10, a.align_center, t.atoms.bg, {top: 0}]}> 73 + <Animated.View 74 + onLayout={e => { 75 + headerHeight.set(e.nativeEvent.layout.height) 76 + }} 77 + style={[headerMinimalShellTransform]}> 78 + {children} 79 + </Animated.View> 80 + </Layout.Center> 107 81 </> 108 82 ) 109 83 } 110 - 111 - const styles = StyleSheet.create({ 112 - bar: { 113 - // @ts-ignore Web only 114 - left: 'calc(50% - 300px)', 115 - width: 600, 116 - borderLeftWidth: 1, 117 - borderRightWidth: 1, 118 - }, 119 - topBar: { 120 - flexDirection: 'row', 121 - justifyContent: 'space-between', 122 - alignItems: 'center', 123 - paddingHorizontal: 18, 124 - paddingTop: 16, 125 - paddingBottom: 8, 126 - }, 127 - tabBar: { 128 - // @ts-ignore Web only 129 - position: 'sticky', 130 - top: 0, 131 - flexDirection: 'column', 132 - alignItems: 'center', 133 - borderLeftWidth: 1, 134 - borderRightWidth: 1, 135 - zIndex: 1, 136 - }, 137 - })
+44 -80
src/view/com/home/HomeHeaderLayoutMobile.tsx
··· 1 1 import React from 'react' 2 - import {StyleSheet, TouchableOpacity, View} from 'react-native' 2 + import {View} from 'react-native' 3 3 import Animated from 'react-native-reanimated' 4 4 import {msg} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 6 7 7 import {HITSLOP_10} from '#/lib/constants' 8 + import {PressableScale} from '#/lib/custom-animations/PressableScale' 9 + import {useHaptics} from '#/lib/haptics' 8 10 import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform' 9 - import {usePalette} from '#/lib/hooks/usePalette' 10 - import {isWeb} from '#/platform/detection' 11 + import {emitSoftReset} from '#/state/events' 11 12 import {useSession} from '#/state/session' 12 - import {useSetDrawerOpen} from '#/state/shell/drawer-open' 13 13 import {useShellLayout} from '#/state/shell/shell-layout' 14 14 import {Logo} from '#/view/icons/Logo' 15 - import {atoms} from '#/alf' 16 - import {useTheme} from '#/alf' 17 - import {atoms as a} from '#/alf' 18 - import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette' 15 + import {atoms as a, useTheme} from '#/alf' 16 + import {ButtonIcon} from '#/components/Button' 19 17 import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag' 20 - import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' 18 + import * as Layout from '#/components/Layout' 21 19 import {Link} from '#/components/Link' 22 - import {IS_DEV} from '#/env' 23 20 24 21 export function HomeHeaderLayoutMobile({ 25 22 children, ··· 28 25 tabBarAnchor: JSX.Element | null | undefined 29 26 }) { 30 27 const t = useTheme() 31 - const pal = usePalette('default') 32 28 const {_} = useLingui() 33 - const setDrawerOpen = useSetDrawerOpen() 34 29 const {headerHeight} = useShellLayout() 35 30 const headerMinimalShellTransform = useMinimalShellHeaderTransform() 36 31 const {hasSession} = useSession() 37 - 38 - const onPressAvi = React.useCallback(() => { 39 - setDrawerOpen(true) 40 - }, [setDrawerOpen]) 32 + const playHaptic = useHaptics() 41 33 42 34 return ( 43 35 <Animated.View 44 - style={[pal.border, styles.tabBar, headerMinimalShellTransform]} 36 + style={[ 37 + a.fixed, 38 + a.z_10, 39 + t.atoms.bg, 40 + { 41 + top: 0, 42 + left: 0, 43 + right: 0, 44 + }, 45 + headerMinimalShellTransform, 46 + ]} 45 47 onLayout={e => { 46 48 headerHeight.set(e.nativeEvent.layout.height) 47 49 }}> 48 - <View style={[pal.view, styles.topBar]}> 49 - <View style={[{width: 100}]}> 50 - <TouchableOpacity 51 - testID="viewHeaderDrawerBtn" 52 - onPress={onPressAvi} 53 - accessibilityRole="button" 54 - accessibilityLabel={_(msg`Open navigation`)} 55 - accessibilityHint={_( 56 - msg`Access profile and other navigation links`, 57 - )} 58 - hitSlop={HITSLOP_10}> 59 - <Menu size="lg" fill={t.atoms.text_contrast_medium.color} /> 60 - </TouchableOpacity> 61 - </View> 62 - <View> 63 - <Logo width={30} /> 50 + <Layout.Header.Outer noBottomBorder> 51 + <Layout.Header.Slot> 52 + <Layout.Header.MenuButton /> 53 + </Layout.Header.Slot> 54 + 55 + <View style={[a.flex_1, a.align_center]}> 56 + <PressableScale 57 + targetScale={0.9} 58 + onPress={() => { 59 + emitSoftReset() 60 + }} 61 + onPressIn={() => { 62 + playHaptic('Heavy') 63 + }} 64 + onPressOut={() => { 65 + playHaptic('Light') 66 + }}> 67 + <Logo width={30} /> 68 + </PressableScale> 64 69 </View> 65 - <View 66 - style={[ 67 - atoms.flex_row, 68 - atoms.justify_end, 69 - atoms.align_center, 70 - atoms.gap_md, 71 - {width: 100}, 72 - ]}> 73 - {IS_DEV && ( 74 - <> 75 - <Link 76 - label="View storybook" 77 - to="/sys/debug" 78 - testID="storybookBtn"> 79 - <ColorPalette size="md" /> 80 - </Link> 81 - </> 82 - )} 70 + 71 + <Layout.Header.Slot> 83 72 {hasSession && ( 84 73 <Link 85 74 testID="viewHeaderHomeFeedPrefsBtn" ··· 93 82 style={[ 94 83 a.justify_center, 95 84 { 96 - marginTop: 2, 97 - marginRight: -6, 85 + marginRight: -Layout.BUTTON_VISUAL_ALIGNMENT_OFFSET, 98 86 }, 99 87 ]}> 100 - <FeedsIcon size="lg" fill={t.atoms.text_contrast_medium.color} /> 88 + <ButtonIcon icon={FeedsIcon} size="lg" /> 101 89 </Link> 102 90 )} 103 - </View> 104 - </View> 91 + </Layout.Header.Slot> 92 + </Layout.Header.Outer> 105 93 {children} 106 94 </Animated.View> 107 95 ) 108 96 } 109 - 110 - const styles = StyleSheet.create({ 111 - tabBar: { 112 - // @ts-ignore web-only 113 - position: isWeb ? 'fixed' : 'absolute', 114 - zIndex: 1, 115 - left: 0, 116 - right: 0, 117 - top: 0, 118 - flexDirection: 'column', 119 - }, 120 - topBar: { 121 - flexDirection: 'row', 122 - justifyContent: 'space-between', 123 - alignItems: 'center', 124 - paddingHorizontal: 16, 125 - paddingVertical: 5, 126 - width: '100%', 127 - minHeight: 46, 128 - }, 129 - title: { 130 - fontSize: 21, 131 - }, 132 - })
+9 -7
src/view/com/lightbox/Lightbox.web.tsx
··· 15 15 } from '@fortawesome/react-native-fontawesome' 16 16 import {msg} from '@lingui/macro' 17 17 import {useLingui} from '@lingui/react' 18 + import {RemoveScrollBar} from 'react-remove-scroll-bar' 18 19 19 - import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 20 20 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 21 21 import {colors, s} from '#/lib/styles' 22 22 import {useLightbox, useLightboxControls} from '#/state/lightbox' ··· 28 28 const {activeLightbox} = useLightbox() 29 29 const {closeLightbox} = useLightboxControls() 30 30 const isActive = !!activeLightbox 31 - useWebBodyScrollLock(isActive) 32 31 33 32 if (!isActive) { 34 33 return null ··· 37 36 const initialIndex = activeLightbox.index 38 37 const imgs = activeLightbox.images 39 38 return ( 40 - <LightboxInner 41 - imgs={imgs} 42 - initialIndex={initialIndex} 43 - onClose={closeLightbox} 44 - /> 39 + <> 40 + <RemoveScrollBar /> 41 + <LightboxInner 42 + imgs={imgs} 43 + initialIndex={initialIndex} 44 + onClose={closeLightbox} 45 + /> 46 + </> 45 47 ) 46 48 } 47 49
+2 -5
src/view/com/lists/MyLists.tsx
··· 15 15 import {cleanError} from '#/lib/strings/errors' 16 16 import {s} from '#/lib/styles' 17 17 import {logger} from '#/logger' 18 - import {isWeb} from '#/platform/detection' 19 18 import {useModerationOpts} from '#/state/preferences/moderation-opts' 20 19 import {MyListsFilter, useMyListsQuery} from '#/state/queries/my-lists' 21 20 import {EmptyState} from '#/view/com/util/EmptyState' ··· 110 109 ) : ( 111 110 <View 112 111 style={[ 113 - (index !== 0 || isWeb) && a.border_t, 112 + index !== 0 && a.border_t, 114 113 t.atoms.border_contrast_low, 115 114 a.px_lg, 116 115 a.py_lg, ··· 141 140 } 142 141 contentContainerStyle={[s.contentContainer]} 143 142 removeClippedSubviews={true} 144 - // @ts-ignore our .web version only -prf 145 - desktopFixedHeight 146 143 /> 147 144 )} 148 145 </View> ··· 160 157 onRefresh={onRefresh} 161 158 contentContainerStyle={[s.contentContainer]} 162 159 removeClippedSubviews={true} 163 - // @ts-ignore our .web version only -prf 164 160 desktopFixedHeight 161 + sideBorders={false} 165 162 /> 166 163 )} 167 164 </View>
+2 -2
src/view/com/modals/Modal.web.tsx
··· 1 1 import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' 2 2 import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' 3 + import {RemoveScrollBar} from 'react-remove-scroll-bar' 3 4 4 5 import {usePalette} from '#/lib/hooks/usePalette' 5 - import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 6 6 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 7 7 import type {Modal as ModalIface} from '#/state/modals' 8 8 import {useModalControls, useModals} from '#/state/modals' ··· 22 22 23 23 export function ModalsContainer() { 24 24 const {isModalActive, activeModals} = useModals() 25 - useWebBodyScrollLock(isModalActive) 26 25 27 26 if (!isModalActive) { 28 27 return null ··· 30 29 31 30 return ( 32 31 <> 32 + <RemoveScrollBar /> 33 33 {activeModals.map((modal, i) => ( 34 34 <Modal key={`modal-${i}`} modal={modal} /> 35 35 ))}
+7 -16
src/view/com/notifications/Feed.tsx
··· 10 10 11 11 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 12 12 import {usePalette} from '#/lib/hooks/usePalette' 13 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 14 13 import {cleanError} from '#/lib/strings/errors' 15 14 import {s} from '#/lib/styles' 16 15 import {logger} from '#/logger' ··· 22 21 import {List, ListRef} from '#/view/com/util/List' 23 22 import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 24 23 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 25 - import {CenteredView} from '#/view/com/util/Views' 26 24 import {FeedItem} from './FeedItem' 27 25 28 26 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} ··· 46 44 47 45 const [isPTRing, setIsPTRing] = React.useState(false) 48 46 const pal = usePalette('default') 49 - const {isTabletOrMobile} = useWebMediaQueries() 50 47 51 48 const {_} = useLingui() 52 49 const moderationOpts = useModerationOpts() ··· 133 130 ) 134 131 } else if (item === LOADING_ITEM) { 135 132 return ( 136 - <View 137 - style={[ 138 - pal.border, 139 - !isTabletOrMobile && {borderTopWidth: StyleSheet.hairlineWidth}, 140 - ]}> 133 + <View style={[pal.border]}> 141 134 <NotificationFeedLoadingPlaceholder /> 142 135 </View> 143 136 ) ··· 146 139 <FeedItem 147 140 item={item} 148 141 moderationOpts={moderationOpts!} 149 - hideTopBorder={index === 0 && isTabletOrMobile} 142 + hideTopBorder={index === 0} 150 143 /> 151 144 ) 152 145 }, 153 - [moderationOpts, isTabletOrMobile, _, onPressRetryLoadMore, pal.border], 146 + [moderationOpts, _, onPressRetryLoadMore, pal.border], 154 147 ) 155 148 156 149 const FeedFooter = React.useCallback( ··· 168 161 return ( 169 162 <View style={s.hContentRegion}> 170 163 {error && ( 171 - <CenteredView> 172 - <ErrorMessage 173 - message={cleanError(error)} 174 - onPressTryAgain={onPressTryAgain} 175 - /> 176 - </CenteredView> 164 + <ErrorMessage 165 + message={cleanError(error)} 166 + onPressTryAgain={onPressTryAgain} 167 + /> 177 168 )} 178 169 <List 179 170 testID="notifsFeed"
+11 -49
src/view/com/pager/PagerWithHeader.web.tsx
··· 1 1 import * as React from 'react' 2 - import {ScrollView, StyleSheet, View} from 'react-native' 2 + import {ScrollView, View} from 'react-native' 3 3 import {useAnimatedRef} from 'react-native-reanimated' 4 4 5 - import {usePalette} from '#/lib/hooks/usePalette' 6 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 7 5 import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager' 6 + import {atoms as a, web} from '#/alf' 7 + import * as Layout from '#/components/Layout' 8 8 import {ListMethods} from '../util/List' 9 9 import {TabBar} from './TabBar' 10 10 ··· 121 121 onSelect?: (index: number) => void 122 122 tabBarAnchor?: JSX.Element | null | undefined 123 123 }): React.ReactNode => { 124 - const pal = usePalette('default') 125 - const {isMobile} = useWebMediaQueries() 126 124 return ( 127 125 <> 128 - <View 129 - style={[ 130 - !isMobile && styles.headerContainerDesktop, 131 - pal.border, 132 - !isHeaderReady && styles.loadingHeader, 133 - ]}> 134 - {renderHeader?.()} 135 - </View> 126 + <Layout.Center>{renderHeader?.()}</Layout.Center> 136 127 {tabBarAnchor} 137 - <View 138 - style={[ 139 - styles.tabBarContainer, 140 - isMobile 141 - ? styles.tabBarContainerMobile 142 - : styles.tabBarContainerDesktop, 143 - pal.border, 128 + <Layout.Center 129 + style={web([ 130 + a.sticky, 131 + a.z_10, 144 132 { 133 + top: 0, 145 134 display: isHeaderReady ? undefined : 'none', 146 135 }, 147 - ]}> 136 + ])}> 148 137 <TabBar 149 138 testID={testID} 150 139 items={items} ··· 154 143 dragProgress={undefined as any /* native-only */} 155 144 dragState={undefined as any /* native-only */} 156 145 /> 157 - </View> 146 + </Layout.Center> 158 147 </> 159 148 ) 160 149 } ··· 179 168 >, 180 169 }) 181 170 } 182 - 183 - const styles = StyleSheet.create({ 184 - headerContainerDesktop: { 185 - marginHorizontal: 'auto', 186 - width: 600, 187 - borderLeftWidth: 1, 188 - borderRightWidth: 1, 189 - }, 190 - tabBarContainer: { 191 - // @ts-ignore web-only 192 - position: 'sticky', 193 - top: 0, 194 - zIndex: 1, 195 - }, 196 - tabBarContainerDesktop: { 197 - marginHorizontal: 'auto', 198 - width: 600, 199 - borderLeftWidth: 1, 200 - borderRightWidth: 1, 201 - }, 202 - tabBarContainerMobile: { 203 - paddingHorizontal: 0, 204 - }, 205 - loadingHeader: { 206 - borderColor: 'transparent', 207 - }, 208 - }) 209 171 210 172 function toArray<T>(v: T | T[]): T[] { 211 173 if (Array.isArray(v)) {
+2 -3
src/view/com/post-thread/PostLikedBy.tsx
··· 6 6 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 7 7 import {cleanError} from '#/lib/strings/errors' 8 8 import {logger} from '#/logger' 9 - import {isWeb} from '#/platform/detection' 10 9 import {useLikedByQuery} from '#/state/queries/post-liked-by' 11 10 import {useResolveUriQuery} from '#/state/queries/resolve-uri' 12 11 import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' ··· 18 17 <ProfileCardWithFollowBtn 19 18 key={item.actor.did} 20 19 profile={item.actor} 21 - noBorder={index === 0 && !isWeb} 20 + noBorder={index === 0} 22 21 /> 23 22 ) 24 23 } ··· 88 87 )} 89 88 errorMessage={cleanError(resolveError || error)} 90 89 sideBorders={false} 90 + topBorder={false} 91 91 /> 92 92 ) 93 93 } ··· 108 108 onRetry={fetchNextPage} 109 109 /> 110 110 } 111 - // @ts-ignore our .web version only -prf 112 111 desktopFixedHeight 113 112 initialNumToRender={initialNumToRender} 114 113 windowSize={11}
+1 -2
src/view/com/post-thread/PostQuotes.tsx
··· 11 11 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' 12 12 import {cleanError} from '#/lib/strings/errors' 13 13 import {logger} from '#/logger' 14 - import {isWeb} from '#/platform/detection' 15 14 import {useModerationOpts} from '#/state/preferences/moderation-opts' 16 15 import {usePostQuotesQuery} from '#/state/queries/post-quotes' 17 16 import {useResolveUriQuery} from '#/state/queries/resolve-uri' ··· 30 29 } 31 30 index: number 32 31 }) { 33 - return <Post post={item.post} hideTopBorder={index === 0 && !isWeb} /> 32 + return <Post post={item.post} hideTopBorder={index === 0} /> 34 33 } 35 34 36 35 function keyExtractor(item: {
+14 -2
src/view/com/post-thread/PostRepostedBy.tsx
··· 12 12 import {List} from '#/view/com/util/List' 13 13 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 14 14 15 - function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) { 16 - return <ProfileCardWithFollowBtn key={item.did} profile={item} /> 15 + function renderItem({ 16 + item, 17 + index, 18 + }: { 19 + item: ActorDefs.ProfileViewBasic 20 + index: number 21 + }) { 22 + return ( 23 + <ProfileCardWithFollowBtn 24 + key={item.did} 25 + profile={item} 26 + noBorder={index === 0} 27 + /> 28 + ) 17 29 } 18 30 19 31 function keyExtractor(item: ActorDefs.ProfileViewBasic) {
+2 -3
src/view/com/post-thread/PostThread.tsx
··· 32 32 import {useSession} from '#/state/session' 33 33 import {useComposerControls} from '#/state/shell' 34 34 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 35 - import {CenteredView} from '#/view/com/util/Views' 36 35 import {atoms as a, useTheme} from '#/alf' 37 36 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 38 37 import {Text} from '#/components/Typography' ··· 484 483 } 485 484 486 485 return ( 487 - <CenteredView style={[a.flex_1]} sideBorders={true}> 486 + <> 488 487 {showHeader && ( 489 488 <ViewHeader 490 489 title={_(msg({message: `Post`, context: 'description'}))} ··· 531 530 {isMobile && canReply && hasSession && ( 532 531 <MobileComposePrompt onPressReply={onPressReply} /> 533 532 )} 534 - </CenteredView> 533 + </> 535 534 ) 536 535 } 537 536
+1 -2
src/view/com/profile/ProfileFollowers.tsx
··· 6 6 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 7 7 import {cleanError} from '#/lib/strings/errors' 8 8 import {logger} from '#/logger' 9 - import {isWeb} from '#/platform/detection' 10 9 import {useProfileFollowersQuery} from '#/state/queries/profile-followers' 11 10 import {useResolveDidQuery} from '#/state/queries/resolve-uri' 12 11 import {useSession} from '#/state/session' ··· 25 24 <ProfileCardWithFollowBtn 26 25 key={item.did} 27 26 profile={item} 28 - noBorder={index === 0 && !isWeb} 27 + noBorder={index === 0} 29 28 /> 30 29 ) 31 30 }
+1 -2
src/view/com/profile/ProfileFollows.tsx
··· 6 6 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 7 7 import {cleanError} from '#/lib/strings/errors' 8 8 import {logger} from '#/logger' 9 - import {isWeb} from '#/platform/detection' 10 9 import {useProfileFollowsQuery} from '#/state/queries/profile-follows' 11 10 import {useResolveDidQuery} from '#/state/queries/resolve-uri' 12 11 import {useSession} from '#/state/session' ··· 25 24 <ProfileCardWithFollowBtn 26 25 key={item.did} 27 26 profile={item} 28 - noBorder={index === 0 && !isWeb} 27 + noBorder={index === 0} 29 28 /> 30 29 ) 31 30 }
+18 -85
src/view/com/profile/ProfileSubpageHeader.tsx
··· 1 1 import React from 'react' 2 - import {Pressable, StyleSheet, View} from 'react-native' 2 + import {Pressable, View} from 'react-native' 3 3 import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' 4 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 4 import {msg, Trans} from '@lingui/macro' 6 5 import {useLingui} from '@lingui/react' 7 6 import {useNavigation} from '@react-navigation/native' 8 7 9 - import {BACK_HITSLOP} from '#/lib/constants' 10 8 import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef' 11 9 import {usePalette} from '#/lib/hooks/usePalette' 12 10 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 13 11 import {makeProfileLink} from '#/lib/routes/links' 14 12 import {NavigationProp} from '#/lib/routes/types' 15 13 import {sanitizeHandle} from '#/lib/strings/handles' 16 - import {isNative} from '#/platform/detection' 17 14 import {emitSoftReset} from '#/state/events' 18 15 import {useLightboxControls} from '#/state/lightbox' 19 - import {useSetDrawerOpen} from '#/state/shell' 20 - import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' 16 + import {TextLink} from '#/view/com/util/Link' 17 + import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 18 + import {Text} from '#/view/com/util/text/Text' 19 + import {UserAvatar, UserAvatarType} from '#/view/com/util/UserAvatar' 21 20 import {StarterPack} from '#/components/icons/StarterPack' 22 - import {TextLink} from '../util/Link' 23 - import {LoadingPlaceholder} from '../util/LoadingPlaceholder' 24 - import {Text} from '../util/text/Text' 25 - import {UserAvatar, UserAvatarType} from '../util/UserAvatar' 26 - import {CenteredView} from '../util/Views' 21 + import * as Layout from '#/components/Layout' 27 22 28 23 export function ProfileSubpageHeader({ 29 24 isLoading, ··· 48 43 | undefined 49 44 avatarType: UserAvatarType | 'starter-pack' 50 45 }>) { 51 - const setDrawerOpen = useSetDrawerOpen() 52 46 const navigation = useNavigation<NavigationProp>() 53 47 const {_} = useLingui() 54 48 const {isMobile} = useWebMediaQueries() ··· 57 51 const canGoBack = navigation.canGoBack() 58 52 const aviRef = useHandleRef() 59 53 60 - const onPressBack = React.useCallback(() => { 61 - if (navigation.canGoBack()) { 62 - navigation.goBack() 63 - } else { 64 - navigation.navigate('Home') 65 - } 66 - }, [navigation]) 67 - 68 - const onPressMenu = React.useCallback(() => { 69 - setDrawerOpen(true) 70 - }, [setDrawerOpen]) 71 - 72 54 const _openLightbox = React.useCallback( 73 55 (uri: string, thumbRect: MeasuredDimensions | null) => { 74 56 openLightbox({ ··· 106 88 }, [_openLightbox, avatar, aviRef]) 107 89 108 90 return ( 109 - <CenteredView style={pal.view}> 110 - {isMobile && ( 111 - <View 112 - style={[ 113 - { 114 - flexDirection: 'row', 115 - alignItems: 'center', 116 - borderBottomWidth: StyleSheet.hairlineWidth, 117 - paddingTop: isNative ? 0 : 8, 118 - paddingBottom: 8, 119 - paddingHorizontal: isMobile ? 12 : 14, 120 - }, 121 - pal.border, 122 - ]}> 123 - <Pressable 124 - testID="headerDrawerBtn" 125 - onPress={canGoBack ? onPressBack : onPressMenu} 126 - hitSlop={BACK_HITSLOP} 127 - style={canGoBack ? styles.backBtn : styles.backBtnWide} 128 - accessibilityRole="button" 129 - accessibilityLabel={canGoBack ? 'Back' : 'Menu'} 130 - accessibilityHint=""> 131 - {canGoBack ? ( 132 - <FontAwesomeIcon 133 - size={18} 134 - icon="angle-left" 135 - style={[styles.backIcon, pal.text]} 136 - /> 137 - ) : ( 138 - <Menu size="lg" style={[{marginTop: 4}, pal.textLight]} /> 139 - )} 140 - </Pressable> 141 - <View style={{flex: 1}} /> 142 - {children} 143 - </View> 144 - )} 91 + <> 92 + <Layout.Header.Outer> 93 + {canGoBack ? ( 94 + <Layout.Header.BackButton /> 95 + ) : ( 96 + <Layout.Header.MenuButton /> 97 + )} 98 + <Layout.Header.Content /> 99 + {children} 100 + </Layout.Header.Outer> 101 + 145 102 <View 146 103 style={{ 147 104 flexDirection: 'row', ··· 206 163 </Text> 207 164 )} 208 165 </View> 209 - {!isMobile && ( 210 - <View 211 - style={{ 212 - flexDirection: 'row', 213 - alignItems: 'center', 214 - }}> 215 - {children} 216 - </View> 217 - )} 218 166 </View> 219 - </CenteredView> 167 + </> 220 168 ) 221 169 } 222 - 223 - const styles = StyleSheet.create({ 224 - backBtn: { 225 - width: 20, 226 - height: 30, 227 - }, 228 - backBtnWide: { 229 - width: 20, 230 - height: 30, 231 - marginRight: 4, 232 - }, 233 - backIcon: { 234 - marginTop: 6, 235 - }, 236 - })
+49 -66
src/view/com/util/List.web.tsx
··· 4 4 5 5 import {batchedUpdates} from '#/lib/batchedUpdates' 6 6 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 7 - import {usePalette} from '#/lib/hooks/usePalette' 8 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 9 7 import {useScrollHandlers} from '#/lib/ScrollContext' 10 8 import {addStyle} from '#/lib/styles' 9 + import * as Layout from '#/components/Layout' 11 10 12 11 export type ListMethods = any // TODO: Better types. 13 12 export type ListProps<ItemT> = Omit< ··· 24 23 desktopFixedHeight?: number | boolean 25 24 // Web only prop to contain the scroll to the container rather than the window 26 25 disableFullWindowScroll?: boolean 26 + /** 27 + * @deprecated Should be using Layout components 28 + */ 27 29 sideBorders?: boolean 28 30 } 29 31 export type ListRef = React.MutableRefObject<any | null> // TODO: Better types. ··· 56 58 renderItem, 57 59 extraData, 58 60 style, 59 - sideBorders = true, 60 61 ...props 61 62 }: ListProps<ItemT>, 62 63 ref: React.Ref<ListMethods>, 63 64 ) { 64 65 const contextScrollHandlers = useScrollHandlers() 65 - const pal = usePalette('default') 66 - const {isMobile} = useWebMediaQueries() 67 - if (!isMobile) { 68 - contentContainerStyle = addStyle( 69 - contentContainerStyle, 70 - styles.containerScroll, 71 - ) 72 - } 73 66 74 67 const isEmpty = !data || data.length === 0 75 68 ··· 326 319 styles.parentTreeVisibilityDetector 327 320 } 328 321 /> 329 - <View 330 - ref={containerRef} 331 - style={[ 332 - !isMobile && sideBorders && styles.sideBorders, 333 - contentContainerStyle, 334 - desktopFixedHeight ? styles.minHeightViewport : null, 335 - pal.border, 336 - ]}> 337 - <Visibility 338 - root={disableFullWindowScroll ? nativeRef : null} 339 - onVisibleChange={handleAboveTheFoldVisibleChange} 340 - style={[styles.aboveTheFoldDetector, {height: headerOffset}]} 341 - /> 342 - {onStartReached && !isEmpty && ( 343 - <EdgeVisibility 322 + <Layout.Center> 323 + <View 324 + ref={containerRef} 325 + style={[ 326 + contentContainerStyle, 327 + desktopFixedHeight ? styles.minHeightViewport : null, 328 + ]}> 329 + <Visibility 344 330 root={disableFullWindowScroll ? nativeRef : null} 345 - onVisibleChange={onHeadVisibilityChange} 346 - topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'} 347 - containerRef={containerRef} 331 + onVisibleChange={handleAboveTheFoldVisibleChange} 332 + style={[styles.aboveTheFoldDetector, {height: headerOffset}]} 348 333 /> 349 - )} 350 - {headerComponent} 351 - {isEmpty 352 - ? emptyComponent 353 - : (data as Array<ItemT>)?.map((item, index) => { 354 - const key = keyExtractor!(item, index) 355 - return ( 356 - <Row<ItemT> 357 - key={key} 358 - item={item} 359 - index={index} 360 - renderItem={renderItem} 361 - extraData={extraData} 362 - onItemSeen={onItemSeen} 363 - /> 364 - ) 365 - })} 366 - {onEndReached && !isEmpty && ( 367 - <EdgeVisibility 368 - root={disableFullWindowScroll ? nativeRef : null} 369 - onVisibleChange={onTailVisibilityChange} 370 - bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'} 371 - containerRef={containerRef} 372 - /> 373 - )} 374 - {footerComponent} 375 - </View> 334 + {onStartReached && !isEmpty && ( 335 + <EdgeVisibility 336 + root={disableFullWindowScroll ? nativeRef : null} 337 + onVisibleChange={onHeadVisibilityChange} 338 + topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'} 339 + containerRef={containerRef} 340 + /> 341 + )} 342 + {headerComponent} 343 + {isEmpty 344 + ? emptyComponent 345 + : (data as Array<ItemT>)?.map((item, index) => { 346 + const key = keyExtractor!(item, index) 347 + return ( 348 + <Row<ItemT> 349 + key={key} 350 + item={item} 351 + index={index} 352 + renderItem={renderItem} 353 + extraData={extraData} 354 + onItemSeen={onItemSeen} 355 + /> 356 + ) 357 + })} 358 + {onEndReached && !isEmpty && ( 359 + <EdgeVisibility 360 + root={disableFullWindowScroll ? nativeRef : null} 361 + onVisibleChange={onTailVisibilityChange} 362 + bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'} 363 + containerRef={containerRef} 364 + /> 365 + )} 366 + {footerComponent} 367 + </View> 368 + </Layout.Center> 376 369 </View> 377 370 ) 378 371 } ··· 558 551 // https://stackoverflow.com/questions/7944460/detect-safari-browser 559 552 560 553 const styles = StyleSheet.create({ 561 - sideBorders: { 562 - borderLeftWidth: 1, 563 - borderRightWidth: 1, 564 - }, 565 - containerScroll: { 566 - width: '100%', 567 - maxWidth: 600, 568 - marginLeft: 'auto', 569 - marginRight: 'auto', 570 - }, 571 554 minHeightViewport: { 572 555 // @ts-ignore web only 573 556 minHeight: '100vh',
+6 -3
src/view/com/util/LoadingScreen.tsx
··· 1 1 import {ActivityIndicator, View} from 'react-native' 2 2 3 3 import {s} from '#/lib/styles' 4 - import {CenteredView} from './Views' 4 + import * as Layout from '#/components/Layout' 5 5 6 + /** 7 + * @deprecated use Layout compoenents directly 8 + */ 6 9 export function LoadingScreen() { 7 10 return ( 8 - <CenteredView> 11 + <Layout.Content> 9 12 <View style={s.p20}> 10 13 <ActivityIndicator size="large" /> 11 14 </View> 12 - </CenteredView> 15 + </Layout.Content> 13 16 ) 14 17 }
-114
src/view/com/util/SimpleViewHeader.tsx
··· 1 - import React from 'react' 2 - import { 3 - StyleProp, 4 - StyleSheet, 5 - TouchableOpacity, 6 - View, 7 - ViewStyle, 8 - } from 'react-native' 9 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 10 - import {useNavigation} from '@react-navigation/native' 11 - 12 - import {usePalette} from '#/lib/hooks/usePalette' 13 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 14 - import {NavigationProp} from '#/lib/routes/types' 15 - import {isWeb} from '#/platform/detection' 16 - import {useSetDrawerOpen} from '#/state/shell' 17 - import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' 18 - import {CenteredView} from './Views' 19 - 20 - const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} 21 - 22 - export function SimpleViewHeader({ 23 - showBackButton = true, 24 - style, 25 - children, 26 - }: React.PropsWithChildren<{ 27 - showBackButton?: boolean 28 - style?: StyleProp<ViewStyle> 29 - }>) { 30 - const pal = usePalette('default') 31 - const setDrawerOpen = useSetDrawerOpen() 32 - const navigation = useNavigation<NavigationProp>() 33 - const {isMobile} = useWebMediaQueries() 34 - const canGoBack = navigation.canGoBack() 35 - 36 - const onPressBack = React.useCallback(() => { 37 - if (navigation.canGoBack()) { 38 - navigation.goBack() 39 - } else { 40 - navigation.navigate('Home') 41 - } 42 - }, [navigation]) 43 - 44 - const onPressMenu = React.useCallback(() => { 45 - setDrawerOpen(true) 46 - }, [setDrawerOpen]) 47 - 48 - const Container = isMobile ? View : CenteredView 49 - return ( 50 - <Container 51 - style={[ 52 - styles.header, 53 - isMobile && styles.headerMobile, 54 - isWeb && styles.headerWeb, 55 - pal.view, 56 - style, 57 - ]}> 58 - {showBackButton ? ( 59 - <TouchableOpacity 60 - testID="viewHeaderDrawerBtn" 61 - onPress={canGoBack ? onPressBack : onPressMenu} 62 - hitSlop={BACK_HITSLOP} 63 - style={canGoBack ? styles.backBtn : styles.backBtnWide} 64 - accessibilityRole="button" 65 - accessibilityLabel={canGoBack ? 'Back' : 'Menu'} 66 - accessibilityHint=""> 67 - {canGoBack ? ( 68 - <FontAwesomeIcon 69 - size={18} 70 - icon="angle-left" 71 - style={[styles.backIcon, pal.text]} 72 - /> 73 - ) : ( 74 - <Menu size="lg" style={[{marginTop: 4}, pal.textLight]} /> 75 - )} 76 - </TouchableOpacity> 77 - ) : null} 78 - {children} 79 - </Container> 80 - ) 81 - } 82 - 83 - const styles = StyleSheet.create({ 84 - header: { 85 - flexDirection: 'row', 86 - alignItems: 'center', 87 - paddingHorizontal: 18, 88 - paddingVertical: 12, 89 - width: '100%', 90 - }, 91 - headerMobile: { 92 - paddingHorizontal: 12, 93 - paddingVertical: 10, 94 - }, 95 - headerWeb: { 96 - // @ts-ignore web-only 97 - position: 'sticky', 98 - top: 0, 99 - zIndex: 1, 100 - }, 101 - backBtn: { 102 - width: 30, 103 - height: 30, 104 - }, 105 - backBtnWide: { 106 - width: 30, 107 - height: 30, 108 - paddingLeft: 4, 109 - marginRight: 4, 110 - }, 111 - backIcon: { 112 - marginTop: 6, 113 - }, 114 - })
+13 -257
src/view/com/util/ViewHeader.tsx
··· 1 - import React from 'react' 2 - import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 - import Animated from 'react-native-reanimated' 4 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 - import {msg} from '@lingui/macro' 6 - import {useLingui} from '@lingui/react' 7 - import {useNavigation} from '@react-navigation/native' 8 - 9 - import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform' 10 - import {usePalette} from '#/lib/hooks/usePalette' 11 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 12 - import {NavigationProp} from '#/lib/routes/types' 13 - import {useSetDrawerOpen} from '#/state/shell' 14 - import {useTheme} from '#/alf' 15 - import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' 16 - import {Text} from './text/Text' 17 - import {CenteredView} from './Views' 1 + import {Header} from '#/components/Layout' 18 2 19 - const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} 20 - 3 + /** 4 + * Legacy ViewHeader component. Use Layout.Header going forward. 5 + * 6 + * @deprecated 7 + */ 21 8 export function ViewHeader({ 22 9 title, 23 - subtitle, 24 - canGoBack, 25 - showBackButton = true, 26 - hideOnScroll, 27 - showOnDesktop, 28 - showBorder, 29 10 renderButton, 30 11 }: { 31 12 title: string 32 13 subtitle?: string 33 - canGoBack?: boolean 34 - showBackButton?: boolean 35 - hideOnScroll?: boolean 36 14 showOnDesktop?: boolean 37 15 showBorder?: boolean 38 16 renderButton?: () => JSX.Element 39 17 }) { 40 - const pal = usePalette('default') 41 - const {_} = useLingui() 42 - const setDrawerOpen = useSetDrawerOpen() 43 - const navigation = useNavigation<NavigationProp>() 44 - const {isDesktop, isTablet} = useWebMediaQueries() 45 - const t = useTheme() 46 - 47 - const onPressBack = React.useCallback(() => { 48 - if (navigation.canGoBack()) { 49 - navigation.goBack() 50 - } else { 51 - navigation.navigate('Home') 52 - } 53 - }, [navigation]) 54 - 55 - const onPressMenu = React.useCallback(() => { 56 - setDrawerOpen(true) 57 - }, [setDrawerOpen]) 58 - 59 - if (isDesktop) { 60 - if (showOnDesktop) { 61 - return ( 62 - <DesktopWebHeader 63 - title={title} 64 - subtitle={subtitle} 65 - renderButton={renderButton} 66 - showBorder={showBorder} 67 - /> 68 - ) 69 - } 70 - return null 71 - } else { 72 - if (typeof canGoBack === 'undefined') { 73 - canGoBack = navigation.canGoBack() 74 - } 75 - 76 - return ( 77 - <Container hideOnScroll={hideOnScroll || false} showBorder={showBorder}> 78 - <View style={{flex: 1}}> 79 - <View style={{flexDirection: 'row', alignItems: 'center'}}> 80 - {showBackButton ? ( 81 - <TouchableOpacity 82 - testID="viewHeaderDrawerBtn" 83 - onPress={canGoBack ? onPressBack : onPressMenu} 84 - hitSlop={BACK_HITSLOP} 85 - style={canGoBack ? styles.backBtn : styles.backBtnWide} 86 - accessibilityRole="button" 87 - accessibilityLabel={canGoBack ? _(msg`Back`) : _(msg`Menu`)} 88 - accessibilityHint={ 89 - canGoBack ? '' : _(msg`Access navigation links and settings`) 90 - }> 91 - {canGoBack ? ( 92 - <FontAwesomeIcon 93 - size={18} 94 - icon="angle-left" 95 - style={[styles.backIcon, pal.text]} 96 - /> 97 - ) : !isTablet ? ( 98 - <Menu size="lg" style={[{marginTop: 3}, pal.textLight]} /> 99 - ) : null} 100 - </TouchableOpacity> 101 - ) : null} 102 - <View style={styles.titleContainer} pointerEvents="none"> 103 - <Text emoji type="title" style={[pal.text, styles.title]}> 104 - {title} 105 - </Text> 106 - </View> 107 - {renderButton ? ( 108 - renderButton() 109 - ) : showBackButton ? ( 110 - <View style={canGoBack ? styles.backBtn : styles.backBtnWide} /> 111 - ) : null} 112 - </View> 113 - {subtitle ? ( 114 - <View 115 - style={[styles.titleContainer, {marginTop: -3}]} 116 - pointerEvents="none"> 117 - <Text 118 - style={[ 119 - pal.text, 120 - styles.subtitle, 121 - t.atoms.text_contrast_medium, 122 - ]}> 123 - {subtitle} 124 - </Text> 125 - </View> 126 - ) : undefined} 127 - </View> 128 - </Container> 129 - ) 130 - } 131 - } 132 - 133 - function DesktopWebHeader({ 134 - title, 135 - subtitle, 136 - renderButton, 137 - showBorder = true, 138 - }: { 139 - title: string 140 - subtitle?: string 141 - renderButton?: () => JSX.Element 142 - showBorder?: boolean 143 - }) { 144 - const pal = usePalette('default') 145 - const t = useTheme() 146 18 return ( 147 - <CenteredView 148 - style={[ 149 - styles.header, 150 - styles.desktopHeader, 151 - pal.border, 152 - { 153 - borderBottomWidth: showBorder ? StyleSheet.hairlineWidth : 0, 154 - }, 155 - {display: 'flex', flexDirection: 'column'}, 156 - ]}> 157 - <View> 158 - <View style={styles.titleContainer} pointerEvents="none"> 159 - <Text type="title-lg" style={[pal.text, styles.title]}> 160 - {title} 161 - </Text> 162 - </View> 163 - {renderButton?.()} 164 - </View> 165 - {subtitle ? ( 166 - <View> 167 - <View style={[styles.titleContainer]} pointerEvents="none"> 168 - <Text 169 - style={[ 170 - pal.text, 171 - styles.subtitleDesktop, 172 - t.atoms.text_contrast_medium, 173 - ]}> 174 - {subtitle} 175 - </Text> 176 - </View> 177 - </View> 178 - ) : null} 179 - </CenteredView> 19 + <Header.Outer> 20 + <Header.BackButton /> 21 + <Header.Content> 22 + <Header.TitleText>{title}</Header.TitleText> 23 + </Header.Content> 24 + <Header.Slot>{renderButton?.() ?? null}</Header.Slot> 25 + </Header.Outer> 180 26 ) 181 27 } 182 - 183 - function Container({ 184 - children, 185 - hideOnScroll, 186 - showBorder, 187 - }: { 188 - children: React.ReactNode 189 - hideOnScroll: boolean 190 - showBorder?: boolean 191 - }) { 192 - const pal = usePalette('default') 193 - const headerMinimalShellTransform = useMinimalShellHeaderTransform() 194 - 195 - if (!hideOnScroll) { 196 - return ( 197 - <View 198 - style={[ 199 - styles.header, 200 - pal.view, 201 - pal.border, 202 - showBorder && styles.border, 203 - ]}> 204 - {children} 205 - </View> 206 - ) 207 - } 208 - return ( 209 - <Animated.View 210 - style={[ 211 - styles.header, 212 - styles.headerFloating, 213 - pal.view, 214 - pal.border, 215 - headerMinimalShellTransform, 216 - showBorder && styles.border, 217 - ]}> 218 - {children} 219 - </Animated.View> 220 - ) 221 - } 222 - 223 - const styles = StyleSheet.create({ 224 - header: { 225 - flexDirection: 'row', 226 - paddingHorizontal: 12, 227 - paddingVertical: 6, 228 - width: '100%', 229 - }, 230 - headerFloating: { 231 - position: 'absolute', 232 - top: 0, 233 - width: '100%', 234 - }, 235 - desktopHeader: { 236 - paddingVertical: 12, 237 - maxWidth: 600, 238 - marginLeft: 'auto', 239 - marginRight: 'auto', 240 - }, 241 - border: { 242 - borderBottomWidth: StyleSheet.hairlineWidth, 243 - }, 244 - titleContainer: { 245 - marginLeft: 'auto', 246 - marginRight: 'auto', 247 - alignItems: 'center', 248 - }, 249 - title: { 250 - fontWeight: '600', 251 - }, 252 - subtitle: { 253 - fontSize: 13, 254 - }, 255 - subtitleDesktop: { 256 - fontSize: 15, 257 - }, 258 - backBtn: { 259 - width: 30, 260 - height: 30, 261 - }, 262 - backBtnWide: { 263 - width: 30, 264 - height: 30, 265 - paddingLeft: 4, 266 - marginRight: 4, 267 - }, 268 - backIcon: { 269 - marginTop: 6, 270 - }, 271 - })
+7
src/view/com/util/Views.tsx
··· 15 15 FlatListComponent<ItemT, FlatListPropsWithLayout<ItemT>>, 16 16 'CellRendererComponent' 17 17 > 18 + 19 + /** 20 + * @deprecated use `Layout` components 21 + */ 18 22 export const ScrollView = Animated.ScrollView 19 23 export type ScrollView = typeof Animated.ScrollView 20 24 25 + /** 26 + * @deprecated use `Layout` components 27 + */ 21 28 export const CenteredView = forwardRef< 22 29 View, 23 30 React.PropsWithChildren<
+8 -23
src/view/com/util/Views.web.tsx
··· 31 31 desktopFixedHeight?: boolean | number 32 32 } 33 33 34 + /** 35 + * @deprecated use `Layout` components 36 + */ 34 37 export const CenteredView = React.forwardRef(function CenteredView( 35 38 { 36 39 style, 37 - sideBorders, 38 40 topBorder, 39 41 ...props 40 42 }: React.PropsWithChildren< ··· 47 49 if (!isMobile) { 48 50 style = addStyle(style, styles.container) 49 51 } 50 - if (sideBorders && !isMobile) { 51 - style = addStyle(style, { 52 - borderLeftWidth: StyleSheet.hairlineWidth, 53 - borderRightWidth: StyleSheet.hairlineWidth, 54 - }) 55 - style = addStyle(style, pal.border) 56 - } 57 52 if (topBorder) { 58 53 style = addStyle(style, { 59 54 borderTopWidth: 1, ··· 75 70 >, 76 71 ref: React.Ref<FlatList<ItemT>>, 77 72 ) { 78 - const pal = usePalette('default') 79 73 const {isMobile} = useWebMediaQueries() 80 74 if (!isMobile) { 81 75 contentContainerStyle = addStyle( ··· 123 117 return ( 124 118 <Animated.FlatList 125 119 ref={ref} 126 - contentContainerStyle={[ 127 - styles.contentContainer, 128 - contentContainerStyle, 129 - pal.border, 130 - ]} 120 + contentContainerStyle={[styles.contentContainer, contentContainerStyle]} 131 121 style={style} 132 122 contentOffset={contentOffset} 133 123 {...props} ··· 135 125 ) 136 126 }) 137 127 128 + /** 129 + * @deprecated use `Layout` components 130 + */ 138 131 export const ScrollView = React.forwardRef(function ScrollViewImpl( 139 132 {contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>, 140 133 ref: React.Ref<Animated.ScrollView>, 141 134 ) { 142 - const pal = usePalette('default') 143 - 144 135 const {isMobile} = useWebMediaQueries() 145 136 if (!isMobile) { 146 137 contentContainerStyle = addStyle( ··· 150 141 } 151 142 return ( 152 143 <Animated.ScrollView 153 - contentContainerStyle={[ 154 - styles.contentContainer, 155 - contentContainerStyle, 156 - pal.border, 157 - ]} 144 + contentContainerStyle={[styles.contentContainer, contentContainerStyle]} 158 145 // @ts-ignore something is wrong with the reanimated types -prf 159 146 ref={ref} 160 147 {...props} ··· 164 151 165 152 const styles = StyleSheet.create({ 166 153 contentContainer: { 167 - borderLeftWidth: StyleSheet.hairlineWidth, 168 - borderRightWidth: StyleSheet.hairlineWidth, 169 154 // @ts-ignore web only 170 155 minHeight: '100vh', 171 156 },
+3 -1
src/view/com/util/error/ErrorScreen.tsx
··· 36 36 37 37 return ( 38 38 <> 39 - {showHeader && isMobile && <ViewHeader title="Error" showBorder />} 39 + {showHeader && isMobile && ( 40 + <ViewHeader title={_(msg`Error`)} showBorder /> 41 + )} 40 42 <CenteredView testID={testID} style={[styles.outer, pal.view]}> 41 43 <View style={styles.errorIconContainer}> 42 44 <View
+45 -83
src/view/screens/Feeds.tsx
··· 24 24 import {useComposerControls} from '#/state/shell/composer' 25 25 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 26 26 import {FAB} from '#/view/com/util/fab/FAB' 27 - import {TextLink} from '#/view/com/util/Link' 28 27 import {List, ListMethods} from '#/view/com/util/List' 29 28 import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 30 29 import {Text} from '#/view/com/util/text/Text' 31 - import {ViewHeader} from '#/view/com/util/ViewHeader' 32 30 import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed' 33 31 import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType' 34 32 import {atoms as a, useTheme} from '#/alf' 33 + import {ButtonIcon} from '#/components/Button' 35 34 import {Divider} from '#/components/Divider' 36 35 import * as FeedCard from '#/components/FeedCard' 37 36 import {SearchInput} from '#/components/forms/SearchInput' ··· 40 39 import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' 41 40 import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass' 42 41 import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle' 42 + import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2' 43 43 import * as Layout from '#/components/Layout' 44 + import {Link} from '#/components/Link' 44 45 import * as ListCard from '#/components/ListCard' 45 46 46 47 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'> ··· 102 103 export function FeedsScreen(_props: Props) { 103 104 const pal = usePalette('default') 104 105 const {openComposer} = useComposerControls() 105 - const {isMobile, isTabletOrDesktop} = useWebMediaQueries() 106 + const {isMobile} = useWebMediaQueries() 106 107 const [query, setQuery] = React.useState('') 107 108 const [isPTR, setIsPTR] = React.useState(false) 108 109 const { ··· 374 375 isUserSearching, 375 376 ]) 376 377 377 - const renderHeaderBtn = React.useCallback(() => { 378 - return ( 379 - <View style={styles.headerBtnGroup}> 380 - <TextLink 381 - testID="editFeedsBtn" 382 - type="lg-medium" 383 - href="/settings/saved-feeds" 384 - accessibilityLabel={_(msg`Edit My Feeds`)} 385 - accessibilityHint="" 386 - text={_(msg`Edit`)} 387 - style={[pal.link, a.pr_xs]} 388 - /> 389 - </View> 390 - ) 391 - }, [pal, _]) 392 - 393 378 const searchBarIndex = items.findIndex( 394 379 item => item.type === 'popularFeedsHeader', 395 380 ) ··· 430 415 </View> 431 416 ) 432 417 } else if (item.type === 'savedFeedsHeader') { 433 - return ( 434 - <> 435 - {!isMobile && ( 436 - <View 437 - style={[ 438 - pal.view, 439 - styles.header, 440 - pal.border, 441 - { 442 - borderBottomWidth: 1, 443 - }, 444 - ]}> 445 - <Text type="title-lg" style={[pal.text, s.bold]}> 446 - <Trans>Feeds</Trans> 447 - </Text> 448 - <View style={styles.headerBtnGroup}> 449 - <TextLink 450 - type="lg" 451 - href="/settings/saved-feeds" 452 - accessibilityLabel={_(msg`Edit My Feeds`)} 453 - accessibilityHint="" 454 - text={_(msg`Edit`)} 455 - style={[pal.link]} 456 - /> 457 - </View> 458 - </View> 459 - )} 460 - <FeedsSavedHeader /> 461 - </> 462 - ) 418 + return <FeedsSavedHeader /> 463 419 } else if (item.type === 'savedFeedNoResults') { 464 420 return ( 465 421 <View ··· 530 486 return null 531 487 }, 532 488 [ 533 - isMobile, 534 - pal.view, 535 489 pal.border, 536 - pal.text, 537 490 pal.textLight, 538 - pal.link, 539 - _, 540 491 query, 541 492 onChangeQuery, 542 493 onPressCancelSearch, ··· 547 498 548 499 return ( 549 500 <Layout.Screen testID="FeedsScreen"> 550 - {isMobile && ( 551 - <ViewHeader 552 - title={_(msg`Feeds`)} 553 - renderButton={renderHeaderBtn} 554 - showBorder 501 + <Layout.Center> 502 + <Layout.Header.Outer> 503 + <Layout.Header.BackButton /> 504 + <Layout.Header.Content> 505 + <Layout.Header.TitleText> 506 + <Trans>Feeds</Trans> 507 + </Layout.Header.TitleText> 508 + </Layout.Header.Content> 509 + <Layout.Header.Slot> 510 + <Link 511 + testID="editFeedsBtn" 512 + to="/settings/saved-feeds" 513 + label={_(msg`Edit My Feeds`)} 514 + size="small" 515 + variant="ghost" 516 + color="secondary" 517 + shape="round" 518 + style={[a.justify_center, {right: -3}]}> 519 + <ButtonIcon icon={Gear} size="lg" /> 520 + </Link> 521 + </Layout.Header.Slot> 522 + </Layout.Header.Outer> 523 + 524 + <List 525 + ref={listRef} 526 + data={items} 527 + keyExtractor={item => item.key} 528 + contentContainerStyle={styles.contentContainer} 529 + renderItem={renderItem} 530 + refreshing={isPTR} 531 + onRefresh={isUserSearching ? undefined : onPullToRefresh} 532 + initialNumToRender={10} 533 + onEndReached={onEndReached} 534 + desktopFixedHeight 535 + keyboardShouldPersistTaps="handled" 536 + keyboardDismissMode="on-drag" 537 + sideBorders={false} 555 538 /> 556 - )} 557 - 558 - <List 559 - ref={listRef} 560 - style={[!isTabletOrDesktop && s.flex1, styles.list]} 561 - data={items} 562 - keyExtractor={item => item.key} 563 - contentContainerStyle={styles.contentContainer} 564 - renderItem={renderItem} 565 - refreshing={isPTR} 566 - onRefresh={isUserSearching ? undefined : onPullToRefresh} 567 - initialNumToRender={10} 568 - onEndReached={onEndReached} 569 - // @ts-ignore our .web version only -prf 570 - desktopFixedHeight 571 - scrollIndicatorInsets={{right: 1}} 572 - keyboardShouldPersistTaps="handled" 573 - keyboardDismissMode="on-drag" 574 - /> 539 + </Layout.Center> 575 540 576 541 {hasSession && ( 577 542 <FAB ··· 728 693 }> 729 694 <IconCircle icon={ListSparkle_Stroke2_Corner0_Rounded} size="lg" /> 730 695 <View style={[a.flex_1, a.gap_xs]}> 731 - <Text style={[a.flex_1, a.text_2xl, a.font_bold, t.atoms.text]}> 696 + <Text style={[a.flex_1, a.text_2xl, a.font_heavy, t.atoms.text]}> 732 697 <Trans>My Feeds</Trans> 733 698 </Text> 734 699 <Text style={[t.atoms.text_contrast_high]}> ··· 754 719 size="lg" 755 720 /> 756 721 <View style={[a.flex_1, a.gap_sm]}> 757 - <Text style={[a.flex_1, a.text_2xl, a.font_bold, t.atoms.text]}> 722 + <Text style={[a.flex_1, a.text_2xl, a.font_heavy, t.atoms.text]}> 758 723 <Trans>Discover New Feeds</Trans> 759 724 </Text> 760 725 <Text style={[t.atoms.text_contrast_high]}> ··· 769 734 } 770 735 771 736 const styles = StyleSheet.create({ 772 - list: { 773 - height: '100%', 774 - }, 775 737 contentContainer: { 776 738 paddingBottom: 100, 777 739 },
+26 -46
src/view/screens/Lists.tsx
··· 1 1 import React from 'react' 2 - import {StyleSheet, View} from 'react-native' 3 2 import {AtUri} from '@atproto/api' 4 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 3 import {msg, Trans} from '@lingui/macro' 6 4 import {useLingui} from '@lingui/react' 7 5 import {useFocusEffect, useNavigation} from '@react-navigation/native' 8 6 9 7 import {useEmail} from '#/lib/hooks/useEmail' 10 - import {usePalette} from '#/lib/hooks/usePalette' 11 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 12 8 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 13 9 import {NavigationProp} from '#/lib/routes/types' 14 - import {s} from '#/lib/styles' 15 10 import {useModalControls} from '#/state/modals' 16 11 import {useSetMinimalShellMode} from '#/state/shell' 17 12 import {MyLists} from '#/view/com/lists/MyLists' 18 - import {Button} from '#/view/com/util/forms/Button' 19 - import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' 20 - import {Text} from '#/view/com/util/text/Text' 13 + import {atoms as a} from '#/alf' 14 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 21 15 import {useDialogControl} from '#/components/Dialog' 22 16 import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 17 + import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 23 18 import * as Layout from '#/components/Layout' 24 19 25 20 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'> 26 21 export function ListsScreen({}: Props) { 27 22 const {_} = useLingui() 28 - const pal = usePalette('default') 29 23 const setMinimalShellMode = useSetMinimalShellMode() 30 - const {isMobile} = useWebMediaQueries() 31 24 const navigation = useNavigation<NavigationProp>() 32 25 const {openModal} = useModalControls() 33 26 const {needsEmailVerification} = useEmail() ··· 62 55 63 56 return ( 64 57 <Layout.Screen testID="listsScreen"> 65 - <SimpleViewHeader 66 - showBackButton={isMobile} 67 - style={[ 68 - pal.border, 69 - isMobile 70 - ? {borderBottomWidth: StyleSheet.hairlineWidth} 71 - : { 72 - borderLeftWidth: StyleSheet.hairlineWidth, 73 - borderRightWidth: StyleSheet.hairlineWidth, 74 - }, 75 - ]}> 76 - <View style={{flex: 1}}> 77 - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> 78 - <Trans>User Lists</Trans> 79 - </Text> 80 - <Text style={pal.textLight}> 58 + <Layout.Header.Outer> 59 + <Layout.Header.BackButton /> 60 + <Layout.Header.Content align="left"> 61 + <Layout.Header.TitleText> 62 + <Trans>Lists</Trans> 63 + </Layout.Header.TitleText> 64 + <Layout.Header.SubtitleText> 81 65 <Trans>Public, shareable lists which can drive feeds.</Trans> 82 - </Text> 83 - </View> 84 - <View style={[{marginLeft: 18}, isMobile && {marginLeft: 12}]}> 85 - <Button 86 - testID="newUserListBtn" 87 - type="default" 88 - onPress={onPressNewList} 89 - style={{ 90 - flexDirection: 'row', 91 - alignItems: 'center', 92 - gap: 8, 93 - }}> 94 - <FontAwesomeIcon icon="plus" color={pal.colors.text} /> 95 - <Text type="button" style={pal.text}> 96 - <Trans context="action">New</Trans> 97 - </Text> 98 - </Button> 99 - </View> 100 - </SimpleViewHeader> 101 - <MyLists filter="curate" style={s.flexGrow1} /> 66 + </Layout.Header.SubtitleText> 67 + </Layout.Header.Content> 68 + <Button 69 + label={_(msg`New list`)} 70 + testID="newUserListBtn" 71 + color="secondary" 72 + variant="solid" 73 + size="small" 74 + onPress={onPressNewList}> 75 + <ButtonIcon icon={PlusIcon} /> 76 + <ButtonText> 77 + <Trans context="action">New</Trans> 78 + </ButtonText> 79 + </Button> 80 + </Layout.Header.Outer> 81 + <MyLists filter="curate" style={a.flex_grow} /> 102 82 <VerifyEmailDialog 103 83 reasonText={_( 104 84 msg`Before creating a list, you must first verify your email.`,
+6 -20
src/view/screens/ModerationBlockedAccounts.tsx
··· 23 23 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 24 24 import {Text} from '#/view/com/util/text/Text' 25 25 import {ViewHeader} from '#/view/com/util/ViewHeader' 26 - import {CenteredView} from '#/view/com/util/Views' 27 26 import * as Layout from '#/components/Layout' 28 27 29 28 type Props = NativeStackScreenProps< ··· 97 96 ) 98 97 return ( 99 98 <Layout.Screen testID="blockedAccountsScreen"> 100 - <CenteredView 101 - style={[ 102 - styles.container, 103 - isTabletOrDesktop && styles.containerDesktop, 104 - pal.view, 105 - pal.border, 106 - ]} 107 - testID="blockedAccountsScreen"> 99 + <Layout.Center> 108 100 <ViewHeader title={_(msg`Blocked Accounts`)} showOnDesktop /> 109 101 <Text 110 102 type="sm" ··· 112 104 styles.description, 113 105 pal.text, 114 106 isTabletOrDesktop && styles.descriptionDesktop, 107 + { 108 + marginTop: 20, 109 + }, 115 110 ]}> 116 111 <Trans> 117 112 Blocked accounts cannot reply in your threads, mention you, or ··· 120 115 </Trans> 121 116 </Text> 122 117 {isEmpty ? ( 123 - <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}> 118 + <View style={[pal.border]}> 124 119 {isError ? ( 125 120 <ErrorScreen 126 121 title="Oops!" ··· 166 161 desktopFixedHeight 167 162 /> 168 163 )} 169 - </CenteredView> 164 + </Layout.Center> 170 165 </Layout.Screen> 171 166 ) 172 167 } 173 168 174 169 const styles = StyleSheet.create({ 175 - container: { 176 - flex: 1, 177 - paddingBottom: 100, 178 - }, 179 - containerDesktop: { 180 - borderLeftWidth: 1, 181 - borderRightWidth: 1, 182 - paddingBottom: 0, 183 - }, 184 170 title: { 185 171 textAlign: 'center', 186 172 marginTop: 12,
+25 -39
src/view/screens/ModerationModlists.tsx
··· 1 1 import React from 'react' 2 - import {View} from 'react-native' 3 2 import {AtUri} from '@atproto/api' 4 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 3 import {msg, Trans} from '@lingui/macro' 6 4 import {useLingui} from '@lingui/react' 7 5 import {useFocusEffect, useNavigation} from '@react-navigation/native' 8 6 9 7 import {useEmail} from '#/lib/hooks/useEmail' 10 - import {usePalette} from '#/lib/hooks/usePalette' 11 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 12 8 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 13 9 import {NavigationProp} from '#/lib/routes/types' 14 - import {s} from '#/lib/styles' 15 10 import {useModalControls} from '#/state/modals' 16 11 import {useSetMinimalShellMode} from '#/state/shell' 17 12 import {MyLists} from '#/view/com/lists/MyLists' 18 - import {Button} from '#/view/com/util/forms/Button' 19 - import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' 20 - import {Text} from '#/view/com/util/text/Text' 13 + import {atoms as a} from '#/alf' 14 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 21 15 import {useDialogControl} from '#/components/Dialog' 22 16 import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 17 + import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 23 18 import * as Layout from '#/components/Layout' 24 19 25 20 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'> 26 21 export function ModerationModlistsScreen({}: Props) { 27 22 const {_} = useLingui() 28 - const pal = usePalette('default') 29 23 const setMinimalShellMode = useSetMinimalShellMode() 30 - const {isMobile} = useWebMediaQueries() 31 24 const navigation = useNavigation<NavigationProp>() 32 25 const {openModal} = useModalControls() 33 26 const {needsEmailVerification} = useEmail() ··· 62 55 63 56 return ( 64 57 <Layout.Screen testID="moderationModlistsScreen"> 65 - <SimpleViewHeader 66 - showBackButton={isMobile} 67 - style={ 68 - !isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}] 69 - }> 70 - <View style={{flex: 1}}> 71 - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> 58 + <Layout.Header.Outer> 59 + <Layout.Header.BackButton /> 60 + <Layout.Header.Content align="left"> 61 + <Layout.Header.TitleText> 72 62 <Trans>Moderation Lists</Trans> 73 - </Text> 74 - <Text style={pal.textLight}> 63 + </Layout.Header.TitleText> 64 + <Layout.Header.SubtitleText> 75 65 <Trans> 76 66 Public, shareable lists of users to mute or block in bulk. 77 67 </Trans> 78 - </Text> 79 - </View> 80 - <View style={[{marginLeft: 18}, isMobile && {marginLeft: 12}]}> 81 - <Button 82 - testID="newModListBtn" 83 - type="default" 84 - onPress={onPressNewList} 85 - style={{ 86 - flexDirection: 'row', 87 - alignItems: 'center', 88 - gap: 8, 89 - }}> 90 - <FontAwesomeIcon icon="plus" color={pal.colors.text} /> 91 - <Text type="button" style={pal.text}> 92 - <Trans>New</Trans> 93 - </Text> 94 - </Button> 95 - </View> 96 - </SimpleViewHeader> 97 - <MyLists filter="mod" style={s.flexGrow1} /> 68 + </Layout.Header.SubtitleText> 69 + </Layout.Header.Content> 70 + <Button 71 + label={_(msg`New list`)} 72 + testID="newModListBtn" 73 + color="secondary" 74 + variant="solid" 75 + size="small" 76 + onPress={onPressNewList}> 77 + <ButtonIcon icon={PlusIcon} /> 78 + <ButtonText> 79 + <Trans context="action">New</Trans> 80 + </ButtonText> 81 + </Button> 82 + </Layout.Header.Outer> 83 + <MyLists filter="mod" style={a.flex_grow} /> 98 84 <VerifyEmailDialog 99 85 reasonText={_( 100 86 msg`Before creating a list, you must first verify your email.`,
+7 -21
src/view/screens/ModerationMutedAccounts.tsx
··· 23 23 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 24 24 import {Text} from '#/view/com/util/text/Text' 25 25 import {ViewHeader} from '#/view/com/util/ViewHeader' 26 - import {CenteredView} from '#/view/com/util/Views' 27 26 import * as Layout from '#/components/Layout' 28 27 29 28 type Props = NativeStackScreenProps< ··· 97 96 ) 98 97 return ( 99 98 <Layout.Screen testID="mutedAccountsScreen"> 100 - <CenteredView 101 - style={[ 102 - styles.container, 103 - isTabletOrDesktop && styles.containerDesktop, 104 - pal.view, 105 - pal.border, 106 - ]} 107 - testID="mutedAccountsScreen"> 108 - <ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop /> 99 + <ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop /> 100 + <Layout.Center> 109 101 <Text 110 102 type="sm" 111 103 style={[ 112 104 styles.description, 113 105 pal.text, 114 106 isTabletOrDesktop && styles.descriptionDesktop, 107 + { 108 + marginTop: 20, 109 + }, 115 110 ]}> 116 111 <Trans> 117 112 Muted accounts have their posts removed from your feed and from your ··· 119 114 </Trans> 120 115 </Text> 121 116 {isEmpty ? ( 122 - <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}> 117 + <View style={[pal.border]}> 123 118 {isError ? ( 124 119 <ErrorScreen 125 120 title="Oops!" ··· 165 160 desktopFixedHeight 166 161 /> 167 162 )} 168 - </CenteredView> 163 + </Layout.Center> 169 164 </Layout.Screen> 170 165 ) 171 166 } 172 167 173 168 const styles = StyleSheet.create({ 174 - container: { 175 - flex: 1, 176 - paddingBottom: 100, 177 - }, 178 - containerDesktop: { 179 - borderLeftWidth: 1, 180 - borderRightWidth: 1, 181 - paddingBottom: 0, 182 - }, 183 169 title: { 184 170 textAlign: 'center', 185 171 marginTop: 12,
+65 -113
src/view/screens/Notifications.tsx
··· 1 - import React, {useCallback} from 'react' 1 + import React from 'react' 2 2 import {View} from 'react-native' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' ··· 6 6 import {useQueryClient} from '@tanstack/react-query' 7 7 8 8 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 9 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 10 9 import {ComposeIcon2} from '#/lib/icons' 11 10 import { 12 11 NativeStackScreenProps, ··· 14 13 } from '#/lib/routes/types' 15 14 import {s} from '#/lib/styles' 16 15 import {logger} from '#/logger' 17 - import {isNative} from '#/platform/detection' 16 + import {isNative, isWeb} from '#/platform/detection' 18 17 import {emitSoftReset, listenSoftReset} from '#/state/events' 19 18 import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed' 20 19 import { ··· 29 28 import {ListMethods} from '#/view/com/util/List' 30 29 import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' 31 30 import {MainScrollProvider} from '#/view/com/util/MainScrollProvider' 32 - import {ViewHeader} from '#/view/com/util/ViewHeader' 33 - import {CenteredView} from '#/view/com/util/Views' 34 - import {atoms as a, useTheme} from '#/alf' 35 - import {Button} from '#/components/Button' 31 + import {atoms as a, useBreakpoints, useTheme} from '#/alf' 32 + import {Button, ButtonIcon} from '#/components/Button' 36 33 import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2' 37 34 import * as Layout from '#/components/Layout' 38 35 import {Link} from '#/components/Link' 39 36 import {Loader} from '#/components/Loader' 40 - import {Text} from '#/components/Typography' 41 37 42 38 type Props = NativeStackScreenProps< 43 39 NotificationsTabNavigatorParams, 44 40 'Notifications' 45 41 > 46 42 export function NotificationsScreen({route: {params}}: Props) { 43 + const t = useTheme() 44 + const {gtTablet} = useBreakpoints() 47 45 const {_} = useLingui() 48 46 const setMinimalShellMode = useSetMinimalShellMode() 49 47 const [isScrolledDown, setIsScrolledDown] = React.useState(false) 50 48 const [isLoadingLatest, setIsLoadingLatest] = React.useState(false) 51 49 const scrollElRef = React.useRef<ListMethods>(null) 52 - const t = useTheme() 53 - const {isDesktop} = useWebMediaQueries() 54 50 const queryClient = useQueryClient() 55 51 const unreadNotifs = useUnreadNotifications() 56 52 const unreadApi = useUnreadNotificationsApi() ··· 110 106 return listenSoftReset(onPressLoadLatest) 111 107 }, [onPressLoadLatest, isScreenFocused]) 112 108 113 - const renderButton = useCallback(() => { 114 - return ( 115 - <Link 116 - to="/notifications/settings" 117 - label={_(msg`Notification settings`)} 118 - size="small" 119 - variant="ghost" 120 - color="secondary" 121 - shape="square" 122 - style={[a.justify_center]}> 123 - <SettingsIcon size="md" style={t.atoms.text_contrast_medium} /> 124 - </Link> 125 - ) 126 - }, [_, t]) 127 - 128 - const ListHeaderComponent = React.useCallback(() => { 129 - if (isDesktop) { 130 - return ( 131 - <View 132 - style={[ 133 - t.atoms.bg, 134 - a.flex_row, 135 - a.align_center, 136 - a.justify_between, 137 - a.gap_lg, 138 - a.px_lg, 139 - a.pr_md, 140 - a.py_sm, 141 - ]}> 109 + return ( 110 + <Layout.Screen testID="notificationsScreen"> 111 + <Layout.Header.Outer> 112 + <Layout.Header.MenuButton /> 113 + <Layout.Header.Content> 142 114 <Button 143 115 label={_(msg`Notifications`)} 144 116 accessibilityHint={_(msg`Refresh notifications`)} 145 - onPress={emitSoftReset}> 146 - {({hovered, pressed}) => ( 147 - <Text 148 - style={[ 149 - a.text_2xl, 150 - a.font_bold, 151 - (hovered || pressed) && a.underline, 152 - ]}> 117 + onPress={emitSoftReset} 118 + style={[a.justify_start]}> 119 + {({hovered}) => ( 120 + <Layout.Header.TitleText 121 + style={[a.w_full, hovered && a.underline]}> 153 122 <Trans>Notifications</Trans> 154 - {hasNew && ( 123 + {isWeb && gtTablet && hasNew && ( 155 124 <View 156 - style={{ 157 - left: 4, 158 - top: -8, 159 - backgroundColor: t.palette.primary_500, 160 - width: 8, 161 - height: 8, 162 - borderRadius: 4, 163 - }} 125 + style={[ 126 + a.rounded_full, 127 + { 128 + width: 8, 129 + height: 8, 130 + bottom: 3, 131 + left: 6, 132 + backgroundColor: t.palette.primary_500, 133 + }, 134 + ]} 164 135 /> 165 136 )} 166 - </Text> 137 + </Layout.Header.TitleText> 167 138 )} 168 139 </Button> 169 - <View style={[a.flex_row, a.align_center, a.gap_sm]}> 170 - {isLoadingLatest ? <Loader size="md" /> : <></>} 171 - {renderButton()} 172 - </View> 173 - </View> 174 - ) 175 - } 176 - return <></> 177 - }, [isDesktop, t, hasNew, renderButton, _, isLoadingLatest]) 140 + </Layout.Header.Content> 141 + <Layout.Header.Slot> 142 + <Link 143 + to="/notifications/settings" 144 + label={_(msg`Notification settings`)} 145 + size="small" 146 + variant="ghost" 147 + color="secondary" 148 + shape="round" 149 + style={[a.justify_center]}> 150 + <ButtonIcon 151 + icon={isLoadingLatest ? Loader : SettingsIcon} 152 + size="lg" 153 + /> 154 + </Link> 155 + </Layout.Header.Slot> 156 + </Layout.Header.Outer> 178 157 179 - const renderHeaderSpinner = React.useCallback(() => { 180 - return ( 181 - <View 182 - style={[ 183 - {width: 30, height: 20}, 184 - a.flex_row, 185 - a.align_center, 186 - a.justify_end, 187 - a.gap_md, 188 - ]}> 189 - {isLoadingLatest ? <Loader width={20} /> : <></>} 190 - {renderButton()} 191 - </View> 192 - ) 193 - }, [renderButton, isLoadingLatest]) 194 - 195 - return ( 196 - <Layout.Screen testID="notificationsScreen"> 197 - <CenteredView style={[a.flex_1, {paddingTop: 2}]} sideBorders={true}> 198 - <ViewHeader 199 - title={_(msg`Notifications`)} 200 - canGoBack={false} 201 - showBorder={true} 202 - renderButton={renderHeaderSpinner} 158 + <MainScrollProvider> 159 + <Feed 160 + onScrolledDownChange={setIsScrolledDown} 161 + scrollElRef={scrollElRef} 162 + overridePriorityNotifications={params?.show === 'all'} 203 163 /> 204 - <MainScrollProvider> 205 - <Feed 206 - onScrolledDownChange={setIsScrolledDown} 207 - scrollElRef={scrollElRef} 208 - ListHeaderComponent={ListHeaderComponent} 209 - overridePriorityNotifications={params?.show === 'all'} 210 - /> 211 - </MainScrollProvider> 212 - {(isScrolledDown || hasNew) && ( 213 - <LoadLatestBtn 214 - onPress={onPressLoadLatest} 215 - label={_(msg`Load new notifications`)} 216 - showIndicator={hasNew} 217 - /> 218 - )} 219 - <FAB 220 - testID="composeFAB" 221 - onPress={() => openComposer({})} 222 - icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} 223 - accessibilityRole="button" 224 - accessibilityLabel={_(msg`New post`)} 225 - accessibilityHint="" 164 + </MainScrollProvider> 165 + {(isScrolledDown || hasNew) && ( 166 + <LoadLatestBtn 167 + onPress={onPressLoadLatest} 168 + label={_(msg`Load new notifications`)} 169 + showIndicator={hasNew} 226 170 /> 227 - </CenteredView> 171 + )} 172 + <FAB 173 + testID="composeFAB" 174 + onPress={() => openComposer({})} 175 + icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} 176 + accessibilityRole="button" 177 + accessibilityLabel={_(msg`New post`)} 178 + accessibilityHint="" 179 + /> 228 180 </Layout.Screen> 229 181 ) 230 182 }
+1 -5
src/view/screens/PostThread.tsx
··· 1 1 import React from 'react' 2 - import {View} from 'react-native' 3 2 import {useFocusEffect} from '@react-navigation/native' 4 3 5 4 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 6 5 import {makeRecordUri} from '#/lib/strings/url-helpers' 7 6 import {useSetMinimalShellMode} from '#/state/shell' 8 7 import {PostThread as PostThreadComponent} from '#/view/com/post-thread/PostThread' 9 - import {atoms as a} from '#/alf' 10 8 import * as Layout from '#/components/Layout' 11 9 12 10 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'> ··· 24 22 25 23 return ( 26 24 <Layout.Screen testID="postThreadScreen"> 27 - <View style={a.flex_1}> 28 - <PostThreadComponent uri={uri} /> 29 - </View> 25 + <PostThreadComponent uri={uri} /> 30 26 </Layout.Screen> 31 27 ) 32 28 }
+2 -4
src/view/screens/Profile.tsx
··· 40 40 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 41 41 import {FAB} from '#/view/com/util/fab/FAB' 42 42 import {ListRef} from '#/view/com/util/List' 43 - import {CenteredView} from '#/view/com/util/Views' 44 43 import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header' 45 44 import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed' 46 45 import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels' 47 - import {web} from '#/alf' 48 46 import * as Layout from '#/components/Layout' 49 47 import {ScreenHider} from '#/components/moderation/ScreenHider' 50 48 import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks' ··· 116 114 // Most pushes will happen here, since we will have only placeholder data 117 115 if (isLoadingDid || isLoadingProfile || starterPacksQuery.isLoading) { 118 116 return ( 119 - <CenteredView sideBorders style={web({height: '100vh'})}> 117 + <Layout.Content> 120 118 <ProfileHeaderLoading /> 121 - </CenteredView> 119 + </Layout.Content> 122 120 ) 123 121 } 124 122 if (resolveError || profileError) {
+4 -5
src/view/screens/ProfileFeed.tsx
··· 49 49 import {LoadingScreen} from '#/view/com/util/LoadingScreen' 50 50 import {Text} from '#/view/com/util/text/Text' 51 51 import * as Toast from '#/view/com/util/Toast' 52 - import {CenteredView} from '#/view/com/util/Views' 53 52 import {atoms as a, useTheme} from '#/alf' 54 53 import {Button as NewButton, ButtonText} from '#/components/Button' 55 54 import {useRichText} from '#/components/hooks/useRichText' ··· 98 97 if (error) { 99 98 return ( 100 99 <Layout.Screen testID="profileFeedScreenError"> 101 - <CenteredView> 100 + <Layout.Content> 102 101 <View style={[pal.view, pal.border, styles.notFoundContainer]}> 103 102 <Text type="title-lg" style={[pal.text, s.mb10]}> 104 103 <Trans>Could not load feed</Trans> ··· 120 119 </Button> 121 120 </View> 122 121 </View> 123 - </CenteredView> 122 + </Layout.Content> 124 123 </Layout.Screen> 125 124 ) 126 125 } ··· 394 393 ]) 395 394 396 395 return ( 397 - <View style={s.hContentRegion}> 396 + <> 398 397 <ReportDialog 399 398 control={reportDialogControl} 400 399 params={{ ··· 434 433 accessibilityHint="" 435 434 /> 436 435 )} 437 - </View> 436 + </> 438 437 ) 439 438 } 440 439
-2
src/view/screens/ProfileFollowers.tsx
··· 10 10 import {ViewHeader} from '#/view/com/util/ViewHeader' 11 11 import {CenteredView} from '#/view/com/util/Views' 12 12 import * as Layout from '#/components/Layout' 13 - import {ListHeaderDesktop} from '#/components/Lists' 14 13 15 14 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'> 16 15 export const ProfileFollowersScreen = ({route}: Props) => { ··· 27 26 return ( 28 27 <Layout.Screen testID="profileFollowersScreen"> 29 28 <CenteredView sideBorders={true}> 30 - <ListHeaderDesktop title={_(msg`Followers`)} /> 31 29 <ViewHeader title={_(msg`Followers`)} showBorder={!isWeb} /> 32 30 <ProfileFollowersComponent name={name} /> 33 31 </CenteredView>
-2
src/view/screens/ProfileFollows.tsx
··· 10 10 import {ViewHeader} from '#/view/com/util/ViewHeader' 11 11 import {CenteredView} from '#/view/com/util/Views' 12 12 import * as Layout from '#/components/Layout' 13 - import {ListHeaderDesktop} from '#/components/Lists' 14 13 15 14 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'> 16 15 export const ProfileFollowsScreen = ({route}: Props) => { ··· 27 26 return ( 28 27 <Layout.Screen testID="profileFollowsScreen"> 29 28 <CenteredView sideBorders={true}> 30 - <ListHeaderDesktop title={_(msg`Following`)} /> 31 29 <ViewHeader title={_(msg`Following`)} showBorder={!isWeb} /> 32 30 <ProfileFollowsComponent name={name} /> 33 31 </CenteredView>
+4 -6
src/view/screens/ProfileList.tsx
··· 69 69 import {LoadingScreen} from '#/view/com/util/LoadingScreen' 70 70 import {Text} from '#/view/com/util/text/Text' 71 71 import * as Toast from '#/view/com/util/Toast' 72 - import {CenteredView} from '#/view/com/util/Views' 73 72 import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen' 74 73 import {atoms as a, useTheme} from '#/alf' 75 74 import {useDialogControl} from '#/components/Dialog' ··· 107 106 108 107 if (resolveError) { 109 108 return ( 110 - <CenteredView> 109 + <Layout.Content> 111 110 <ErrorScreen 112 111 error={_( 113 112 msg`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`, 114 113 )} 115 114 /> 116 - </CenteredView> 115 + </Layout.Content> 117 116 ) 118 117 } 119 118 if (listError) { 120 119 return ( 121 - <CenteredView> 120 + <Layout.Content> 122 121 <ErrorScreen error={cleanError(listError)} /> 123 - </CenteredView> 122 + </Layout.Content> 124 123 ) 125 124 } 126 125 ··· 1010 1009 pal.view, 1011 1010 pal.border, 1012 1011 { 1013 - marginTop: 10, 1014 1012 paddingHorizontal: 18, 1015 1013 paddingVertical: 14, 1016 1014 borderTopWidth: StyleSheet.hairlineWidth,
+114 -124
src/view/screens/SavedFeeds.tsx
··· 25 25 import {TextLink} from '#/view/com/util/Link' 26 26 import {Text} from '#/view/com/util/text/Text' 27 27 import * as Toast from '#/view/com/util/Toast' 28 - import {ViewHeader} from '#/view/com/util/ViewHeader' 29 - import {CenteredView, ScrollView} from '#/view/com/util/Views' 30 28 import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed' 31 29 import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType' 32 30 import {atoms as a, useTheme} from '#/alf' 33 31 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 34 32 import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' 33 + import {FloppyDisk_Stroke2_Corner0_Rounded as SaveIcon} from '#/components/icons/FloppyDisk' 35 34 import * as Layout from '#/components/Layout' 36 35 import {Loader} from '#/components/Loader' 37 36 ··· 51 50 }) { 52 51 const pal = usePalette('default') 53 52 const {_} = useLingui() 54 - const {isMobile, isTabletOrDesktop, isDesktop} = useWebMediaQueries() 53 + const {isMobile, isDesktop} = useWebMediaQueries() 55 54 const setMinimalShellMode = useSetMinimalShellMode() 56 55 const {mutateAsync: overwriteSavedFeeds, isPending: isOverwritePending} = 57 56 useOverwriteSavedFeedsMutation() ··· 88 87 } 89 88 }, [_, overwriteSavedFeeds, currentFeeds, navigation]) 90 89 91 - const renderHeaderBtn = React.useCallback(() => { 92 - return ( 93 - <Button 94 - size="small" 95 - variant={hasUnsavedChanges ? 'solid' : 'solid'} 96 - color={hasUnsavedChanges ? 'primary' : 'secondary'} 97 - onPress={onSaveChanges} 98 - label={_(msg`Save changes`)} 99 - disabled={isOverwritePending || !hasUnsavedChanges} 100 - style={[isDesktop && a.mt_sm]} 101 - testID="saveChangesBtn"> 102 - <ButtonText style={[isDesktop && a.text_md]}> 103 - {isDesktop ? <Trans>Save changes</Trans> : <Trans>Save</Trans>} 104 - </ButtonText> 105 - {isOverwritePending && <ButtonIcon icon={Loader} />} 106 - </Button> 107 - ) 108 - }, [_, isDesktop, onSaveChanges, hasUnsavedChanges, isOverwritePending]) 109 - 110 90 return ( 111 91 <Layout.Screen> 112 - <CenteredView 113 - style={[a.util_screen_outer]} 114 - sideBorders={isTabletOrDesktop}> 115 - <ViewHeader 116 - title={_(msg`Edit My Feeds`)} 117 - showOnDesktop 118 - showBorder 119 - renderButton={renderHeaderBtn} 120 - /> 121 - <ScrollView style={[a.flex_1]} contentContainerStyle={[a.border_0]}> 122 - {noSavedFeedsOfAnyType && ( 123 - <View style={[pal.border, a.border_b]}> 124 - <NoSavedFeedsOfAnyType /> 125 - </View> 126 - )} 92 + <Layout.Header.Outer> 93 + <Layout.Header.BackButton /> 94 + <Layout.Header.Content align="left"> 95 + <Layout.Header.TitleText> 96 + <Trans>Feeds</Trans> 97 + </Layout.Header.TitleText> 98 + </Layout.Header.Content> 99 + <Button 100 + testID="saveChangesBtn" 101 + size="small" 102 + variant={hasUnsavedChanges ? 'solid' : 'solid'} 103 + color={hasUnsavedChanges ? 'primary' : 'secondary'} 104 + onPress={onSaveChanges} 105 + label={_(msg`Save changes`)} 106 + disabled={isOverwritePending || !hasUnsavedChanges}> 107 + <ButtonIcon icon={isOverwritePending ? Loader : SaveIcon} /> 108 + <ButtonText> 109 + {isDesktop ? <Trans>Save changes</Trans> : <Trans>Save</Trans>} 110 + </ButtonText> 111 + </Button> 112 + </Layout.Header.Outer> 127 113 128 - <View style={[pal.text, pal.border, styles.title]}> 129 - <Text type="title" style={pal.text}> 130 - <Trans>Pinned Feeds</Trans> 131 - </Text> 114 + <Layout.Content> 115 + {noSavedFeedsOfAnyType && ( 116 + <View style={[pal.border, a.border_b]}> 117 + <NoSavedFeedsOfAnyType /> 132 118 </View> 119 + )} 133 120 134 - {preferences ? ( 135 - !pinnedFeeds.length ? ( 136 - <View 137 - style={[ 138 - pal.border, 139 - isMobile && s.flex1, 140 - pal.viewLight, 141 - styles.empty, 142 - ]}> 143 - <Text type="lg" style={[pal.text]}> 144 - <Trans>You don't have any pinned feeds.</Trans> 145 - </Text> 146 - </View> 147 - ) : ( 148 - pinnedFeeds.map(f => ( 149 - <ListItem 150 - key={f.id} 151 - feed={f} 152 - isPinned 153 - currentFeeds={currentFeeds} 154 - setCurrentFeeds={setCurrentFeeds} 155 - preferences={preferences} 156 - /> 157 - )) 158 - ) 159 - ) : ( 160 - <ActivityIndicator style={{marginTop: 20}} /> 161 - )} 121 + <View style={[pal.text, pal.border, styles.title]}> 122 + <Text type="title" style={pal.text}> 123 + <Trans>Pinned Feeds</Trans> 124 + </Text> 125 + </View> 162 126 163 - {noFollowingFeed && ( 164 - <View style={[pal.border, a.border_b]}> 165 - <NoFollowingFeed /> 127 + {preferences ? ( 128 + !pinnedFeeds.length ? ( 129 + <View 130 + style={[ 131 + pal.border, 132 + isMobile && s.flex1, 133 + pal.viewLight, 134 + styles.empty, 135 + ]}> 136 + <Text type="lg" style={[pal.text]}> 137 + <Trans>You don't have any pinned feeds.</Trans> 138 + </Text> 166 139 </View> 167 - )} 140 + ) : ( 141 + pinnedFeeds.map(f => ( 142 + <ListItem 143 + key={f.id} 144 + feed={f} 145 + isPinned 146 + currentFeeds={currentFeeds} 147 + setCurrentFeeds={setCurrentFeeds} 148 + preferences={preferences} 149 + /> 150 + )) 151 + ) 152 + ) : ( 153 + <ActivityIndicator style={{marginTop: 20}} /> 154 + )} 168 155 169 - <View style={[pal.text, pal.border, styles.title]}> 170 - <Text type="title" style={pal.text}> 171 - <Trans>Saved Feeds</Trans> 172 - </Text> 156 + {noFollowingFeed && ( 157 + <View style={[pal.border, a.border_b]}> 158 + <NoFollowingFeed /> 173 159 </View> 174 - {preferences ? ( 175 - !unpinnedFeeds.length ? ( 176 - <View 177 - style={[ 178 - pal.border, 179 - isMobile && s.flex1, 180 - pal.viewLight, 181 - styles.empty, 182 - ]}> 183 - <Text type="lg" style={[pal.text]}> 184 - <Trans>You don't have any saved feeds.</Trans> 185 - </Text> 186 - </View> 187 - ) : ( 188 - unpinnedFeeds.map(f => ( 189 - <ListItem 190 - key={f.id} 191 - feed={f} 192 - isPinned={false} 193 - currentFeeds={currentFeeds} 194 - setCurrentFeeds={setCurrentFeeds} 195 - preferences={preferences} 196 - /> 197 - )) 198 - ) 160 + )} 161 + 162 + <View style={[pal.text, pal.border, styles.title]}> 163 + <Text type="title" style={pal.text}> 164 + <Trans>Saved Feeds</Trans> 165 + </Text> 166 + </View> 167 + {preferences ? ( 168 + !unpinnedFeeds.length ? ( 169 + <View 170 + style={[ 171 + pal.border, 172 + isMobile && s.flex1, 173 + pal.viewLight, 174 + styles.empty, 175 + ]}> 176 + <Text type="lg" style={[pal.text]}> 177 + <Trans>You don't have any saved feeds.</Trans> 178 + </Text> 179 + </View> 199 180 ) : ( 200 - <ActivityIndicator style={{marginTop: 20}} /> 201 - )} 181 + unpinnedFeeds.map(f => ( 182 + <ListItem 183 + key={f.id} 184 + feed={f} 185 + isPinned={false} 186 + currentFeeds={currentFeeds} 187 + setCurrentFeeds={setCurrentFeeds} 188 + preferences={preferences} 189 + /> 190 + )) 191 + ) 192 + ) : ( 193 + <ActivityIndicator style={{marginTop: 20}} /> 194 + )} 202 195 203 - <View style={styles.footerText}> 204 - <Text type="sm" style={pal.textLight}> 205 - <Trans> 206 - Feeds are custom algorithms that users build with a little 207 - coding expertise.{' '} 208 - <TextLink 209 - type="sm" 210 - style={pal.link} 211 - href="https://github.com/bluesky-social/feed-generator" 212 - text={_(msg`See this guide`)} 213 - />{' '} 214 - for more information. 215 - </Trans> 216 - </Text> 217 - </View> 218 - <View style={{height: 100}} /> 219 - </ScrollView> 220 - </CenteredView> 196 + <View style={styles.footerText}> 197 + <Text type="sm" style={pal.textLight}> 198 + <Trans> 199 + Feeds are custom algorithms that users build with a little coding 200 + expertise.{' '} 201 + <TextLink 202 + type="sm" 203 + style={pal.link} 204 + href="https://github.com/bluesky-social/feed-generator" 205 + text={_(msg`See this guide`)} 206 + />{' '} 207 + for more information. 208 + </Trans> 209 + </Text> 210 + </View> 211 + </Layout.Content> 221 212 </Layout.Screen> 222 213 ) 223 214 } ··· 456 447 }, 457 448 footerText: { 458 449 paddingHorizontal: 26, 459 - paddingTop: 22, 460 - paddingBottom: 100, 450 + paddingVertical: 22, 461 451 }, 462 452 })
+105 -131
src/view/screens/Search/Search.tsx
··· 55 55 import {Link} from '#/view/com/util/Link' 56 56 import {List} from '#/view/com/util/List' 57 57 import {Text} from '#/view/com/util/text/Text' 58 - import {CenteredView, ScrollView} from '#/view/com/util/Views' 59 58 import {Explore} from '#/view/screens/Search/Explore' 60 59 import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search' 61 60 import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils' ··· 68 67 import * as Layout from '#/components/Layout' 69 68 70 69 function Loader() { 71 - const pal = usePalette('default') 72 - const {isMobile} = useWebMediaQueries() 73 70 return ( 74 - <CenteredView 75 - style={[ 76 - // @ts-ignore web only -prf 77 - { 78 - padding: 18, 79 - height: isWeb ? '100vh' : undefined, 80 - }, 81 - pal.border, 82 - ]} 83 - sideBorders={!isMobile}> 84 - <ActivityIndicator /> 85 - </CenteredView> 71 + <Layout.Content> 72 + <View style={[a.py_xl]}> 73 + <ActivityIndicator /> 74 + </View> 75 + </Layout.Content> 86 76 ) 87 77 } 88 78 89 79 function EmptyState({message, error}: {message: string; error?: string}) { 90 80 const pal = usePalette('default') 91 - const {isMobile} = useWebMediaQueries() 92 81 93 82 return ( 94 - <CenteredView 95 - sideBorders={!isMobile} 96 - style={[ 97 - pal.border, 98 - // @ts-ignore web only -prf 99 - { 100 - padding: 18, 101 - height: isWeb ? '100vh' : undefined, 102 - }, 103 - ]}> 104 - <View style={[pal.viewLight, {padding: 18, borderRadius: 8}]}> 105 - <Text style={[pal.text]}>{message}</Text> 83 + <Layout.Content> 84 + <View style={[a.p_xl]}> 85 + <View style={[pal.viewLight, {padding: 18, borderRadius: 8}]}> 86 + <Text style={[pal.text]}>{message}</Text> 106 87 107 - {error && ( 108 - <> 109 - <View 110 - style={[ 111 - { 112 - marginVertical: 12, 113 - height: 1, 114 - width: '100%', 115 - backgroundColor: pal.text.color, 116 - opacity: 0.2, 117 - }, 118 - ]} 119 - /> 88 + {error && ( 89 + <> 90 + <View 91 + style={[ 92 + { 93 + marginVertical: 12, 94 + height: 1, 95 + width: '100%', 96 + backgroundColor: pal.text.color, 97 + opacity: 0.2, 98 + }, 99 + ]} 100 + /> 120 101 121 - <Text style={[pal.textLight]}> 122 - <Trans>Error:</Trans> {error} 123 - </Text> 124 - </> 125 - )} 102 + <Text style={[pal.textLight]}> 103 + <Trans>Error:</Trans> {error} 104 + </Text> 105 + </> 106 + )} 107 + </View> 126 108 </View> 127 - </CenteredView> 109 + </Layout.Content> 128 110 ) 129 111 } 130 112 ··· 224 206 if (item.type === 'post') { 225 207 return <Post post={item.post} /> 226 208 } else { 227 - return <Loader /> 209 + return null 228 210 } 229 211 }} 230 212 keyExtractor={item => item.key} ··· 550 532 <Pager 551 533 onPageSelected={onPageSelected} 552 534 renderTabBar={props => ( 553 - <CenteredView 554 - sideBorders 535 + <Layout.Center 555 536 style={[ 556 - pal.border, 557 - pal.view, 558 - web({ 559 - position: isWeb ? 'sticky' : '', 560 - zIndex: 1, 561 - }), 537 + web([a.sticky, a.z_10]), 562 538 {top: isWeb ? headerHeight : undefined}, 563 539 ]}> 564 540 <TabBar items={sections.map(section => section.title)} {...props} /> 565 - </CenteredView> 541 + </Layout.Center> 566 542 )} 567 543 initialPage={0}> 568 544 {sections.map((section, i) => ( ··· 572 548 ) : hasSession ? ( 573 549 <Explore /> 574 550 ) : ( 575 - <CenteredView sideBorders style={pal.border}> 551 + <Layout.Center> 576 552 <View 577 553 // @ts-ignore web only -esb 578 554 style={{ ··· 614 590 </Text> 615 591 </View> 616 592 </View> 617 - </CenteredView> 593 + </Layout.Center> 618 594 ) 619 595 } 620 596 SearchScreenInner = React.memo(SearchScreenInner) ··· 650 626 * Arbitrary sizing, so guess and check, used for sticky header alignment and 651 627 * sizing. 652 628 */ 653 - const headerHeight = 64 + (showFilters ? 40 : 0) 629 + const headerHeight = 60 + (showFilters ? 40 : 0) 654 630 655 631 useFocusEffect( 656 632 useNonReactiveCallback(() => { ··· 861 837 862 838 return ( 863 839 <Layout.Screen testID="searchScreen"> 864 - <CenteredView 840 + <View 865 841 style={[ 866 - a.p_md, 867 - a.pb_sm, 868 - a.gap_sm, 869 - t.atoms.bg, 870 842 web({ 871 843 height: headerHeight, 872 844 position: 'sticky', 873 845 top: 0, 874 846 zIndex: 1, 875 847 }), 876 - ]} 877 - sideBorders={gtMobile}> 878 - <View style={[a.flex_row, a.gap_sm]}> 879 - {!gtMobile && ( 880 - <Button 881 - testID="viewHeaderBackOrMenuBtn" 882 - onPress={onPressMenu} 883 - hitSlop={HITSLOP_10} 884 - label={_(msg`Menu`)} 885 - accessibilityHint={_(msg`Access navigation links and settings`)} 886 - size="large" 887 - variant="solid" 888 - color="secondary" 889 - shape="square"> 890 - <ButtonIcon icon={Menu} size="lg" /> 891 - </Button> 892 - )} 893 - <View style={[a.flex_1]}> 894 - <SearchInput 895 - ref={textInput} 896 - value={searchText} 897 - onFocus={onSearchInputFocus} 898 - onChangeText={onChangeText} 899 - onClearText={onPressClearQuery} 900 - onSubmitEditing={onSubmit} 901 - /> 902 - </View> 903 - {showAutocomplete && ( 904 - <Button 905 - label={_(msg`Cancel search`)} 906 - size="large" 907 - variant="ghost" 908 - color="secondary" 909 - style={[a.px_sm]} 910 - onPress={onPressCancelSearch} 911 - hitSlop={HITSLOP_10}> 912 - <ButtonText> 913 - <Trans>Cancel</Trans> 914 - </ButtonText> 915 - </Button> 916 - )} 917 - </View> 918 - 919 - {showFilters && ( 920 - <View 921 - style={[a.flex_row, a.align_center, a.justify_between, a.gap_sm]}> 922 - <View style={[{width: 140}]}> 923 - <SearchLanguageDropdown 924 - value={params.lang} 925 - onChange={params.setLang} 926 - /> 848 + ]}> 849 + <Layout.Center> 850 + <View style={[a.p_md, a.pb_sm, a.gap_sm, t.atoms.bg]}> 851 + <View style={[a.flex_row, a.gap_sm]}> 852 + {!gtMobile && ( 853 + <Button 854 + testID="viewHeaderBackOrMenuBtn" 855 + onPress={onPressMenu} 856 + hitSlop={HITSLOP_10} 857 + label={_(msg`Menu`)} 858 + accessibilityHint={_( 859 + msg`Access navigation links and settings`, 860 + )} 861 + size="large" 862 + variant="solid" 863 + color="secondary" 864 + shape="square"> 865 + <ButtonIcon icon={Menu} size="lg" /> 866 + </Button> 867 + )} 868 + <View style={[a.flex_1]}> 869 + <SearchInput 870 + ref={textInput} 871 + value={searchText} 872 + onFocus={onSearchInputFocus} 873 + onChangeText={onChangeText} 874 + onClearText={onPressClearQuery} 875 + onSubmitEditing={onSubmit} 876 + /> 877 + </View> 878 + {showAutocomplete && ( 879 + <Button 880 + label={_(msg`Cancel search`)} 881 + size="large" 882 + variant="ghost" 883 + color="secondary" 884 + style={[a.px_sm]} 885 + onPress={onPressCancelSearch} 886 + hitSlop={HITSLOP_10}> 887 + <ButtonText> 888 + <Trans>Cancel</Trans> 889 + </ButtonText> 890 + </Button> 891 + )} 927 892 </View> 893 + 894 + {showFilters && ( 895 + <View 896 + style={[ 897 + a.flex_row, 898 + a.align_center, 899 + a.justify_between, 900 + a.gap_sm, 901 + ]}> 902 + <View style={[{width: 140}]}> 903 + <SearchLanguageDropdown 904 + value={params.lang} 905 + onChange={params.setLang} 906 + /> 907 + </View> 908 + </View> 909 + )} 928 910 </View> 929 - )} 930 - </CenteredView> 911 + </Layout.Center> 912 + </View> 931 913 932 914 <View 933 915 style={{ ··· 992 974 !moderationOpts ? ( 993 975 <Loader /> 994 976 ) : ( 995 - <ScrollView 996 - style={{height: '100%'}} 997 - // @ts-ignore web only -prf 998 - dataSet={{stableGutters: '1'}} 977 + <Layout.Content 999 978 keyboardShouldPersistTaps="handled" 1000 979 keyboardDismissMode="on-drag"> 1001 980 <SearchLinkCard ··· 1020 999 /> 1021 1000 ))} 1022 1001 <View style={{height: 200}} /> 1023 - </ScrollView> 1002 + </Layout.Content> 1024 1003 )} 1025 1004 </> 1026 1005 ) ··· 1042 1021 onRemoveItemClick: (item: string) => void 1043 1022 onRemoveProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void 1044 1023 }) { 1045 - const {isTabletOrDesktop, isMobile} = useWebMediaQueries() 1024 + const {isMobile} = useWebMediaQueries() 1046 1025 const pal = usePalette('default') 1047 1026 const {_} = useLingui() 1048 1027 1049 1028 return ( 1050 - <CenteredView 1051 - sideBorders={isTabletOrDesktop} 1052 - // @ts-ignore web only -prf 1053 - style={{ 1054 - height: isWeb ? '100vh' : undefined, 1055 - }}> 1029 + <Layout.Content> 1056 1030 <View style={styles.searchHistoryContainer}> 1057 1031 {(searchHistory.length > 0 || selectedProfiles.length > 0) && ( 1058 1032 <Text style={[pal.text, styles.searchHistoryTitle]}> ··· 1152 1126 </View> 1153 1127 )} 1154 1128 </View> 1155 - </CenteredView> 1129 + </Layout.Content> 1156 1130 ) 1157 1131 } 1158 1132
+7 -4
src/view/shell/Composer.web.tsx
··· 3 3 import {DismissableLayer} from '@radix-ui/react-dismissable-layer' 4 4 import {useFocusGuards} from '@radix-ui/react-focus-guards' 5 5 import {FocusScope} from '@radix-ui/react-focus-scope' 6 + import {RemoveScrollBar} from 'react-remove-scroll-bar' 6 7 7 - import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 8 8 import {useModals} from '#/state/modals' 9 9 import {ComposerOpts, useComposerState} from '#/state/shell/composer' 10 10 import { ··· 20 20 const state = useComposerState() 21 21 const isActive = !!state 22 22 23 - useWebBodyScrollLock(isActive) 24 - 25 23 // rendering 26 24 // = 27 25 ··· 29 27 return <View /> 30 28 } 31 29 32 - return <Inner state={state} /> 30 + return ( 31 + <> 32 + <RemoveScrollBar /> 33 + <Inner state={state} /> 34 + </> 35 + ) 33 36 } 34 37 35 38 function Inner({state}: {state: ComposerOpts}) {
+53 -78
src/view/shell/desktop/LeftNav.tsx
··· 1 1 import React from 'react' 2 - import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 - import { 4 - FontAwesomeIcon, 5 - FontAwesomeIconStyle, 6 - } from '@fortawesome/react-native-fontawesome' 2 + import {StyleSheet, View} from 'react-native' 3 + import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' 7 4 import {msg, Trans} from '@lingui/macro' 8 5 import {useLingui} from '@lingui/react' 9 6 import { ··· 14 11 15 12 import {usePalette} from '#/lib/hooks/usePalette' 16 13 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 17 - import {getCurrentRoute, isStateAtTabRoot, isTab} from '#/lib/routes/helpers' 14 + import {getCurrentRoute, isTab} from '#/lib/routes/helpers' 18 15 import {makeProfileLink} from '#/lib/routes/links' 19 - import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' 16 + import {CommonNavigatorParams} from '#/lib/routes/types' 20 17 import {isInvalidHandle} from '#/lib/strings/handles' 21 18 import {emitSoftReset} from '#/state/events' 22 19 import {useFetchHandle} from '#/state/queries/handle' ··· 101 98 ) 102 99 } 103 100 104 - const HIDDEN_BACK_BNT_ROUTES = ['StarterPackWizard', 'StarterPackEdit'] 105 - 106 - function BackBtn() { 107 - const {isTablet} = useWebMediaQueries() 108 - const pal = usePalette('default') 109 - const navigation = useNavigation<NavigationProp>() 110 - const {_} = useLingui() 111 - const shouldShow = useNavigationState( 112 - state => 113 - !isStateAtTabRoot(state) && 114 - !HIDDEN_BACK_BNT_ROUTES.includes(getCurrentRoute(state).name), 115 - ) 116 - 117 - const onPressBack = React.useCallback(() => { 118 - if (navigation.canGoBack()) { 119 - navigation.goBack() 120 - } else { 121 - navigation.navigate('Home') 122 - } 123 - }, [navigation]) 124 - 125 - if (!shouldShow || isTablet) { 126 - return <></> 127 - } 128 - return ( 129 - <TouchableOpacity 130 - testID="viewHeaderBackOrMenuBtn" 131 - onPress={onPressBack} 132 - style={styles.backBtn} 133 - accessibilityRole="button" 134 - accessibilityLabel={_(msg`Go back`)} 135 - accessibilityHint=""> 136 - <FontAwesomeIcon 137 - size={24} 138 - icon="angle-left" 139 - style={pal.text as FontAwesomeIconStyle} 140 - /> 141 - </TouchableOpacity> 142 - ) 143 - } 144 - 145 101 interface NavItemProps { 146 102 count?: string 147 103 href: string ··· 220 176 ]}> 221 177 {isCurrent ? iconFilled : icon} 222 178 {typeof count === 'string' && count ? ( 223 - <Text 224 - accessibilityLabel={_(msg`${count} unread items`)} 225 - accessibilityHint="" 226 - accessible={true} 179 + <View 227 180 style={[ 228 181 a.absolute, 229 - a.text_xs, 230 - a.font_bold, 231 - a.rounded_full, 232 - a.text_center, 233 - { 234 - top: '-10%', 235 - left: count.length === 1 ? '50%' : '40%', 236 - backgroundColor: t.palette.primary_500, 237 - color: t.palette.white, 238 - lineHeight: a.text_sm.fontSize, 239 - paddingHorizontal: 4, 240 - paddingVertical: 1, 241 - minWidth: 16, 242 - }, 243 - isTablet && [ 182 + a.inset_0, 183 + {right: -20}, // more breathing room 184 + ]}> 185 + <Text 186 + accessibilityLabel={_(msg`${count} unread items`)} 187 + accessibilityHint="" 188 + accessible={true} 189 + numberOfLines={1} 190 + style={[ 191 + a.absolute, 192 + a.text_xs, 193 + a.font_bold, 194 + a.rounded_full, 195 + a.text_center, 196 + a.leading_tight, 244 197 { 245 - top: '10%', 246 - left: count.length === 1 ? '50%' : '40%', 198 + top: '-10%', 199 + left: count.length === 1 ? 12 : 8, 200 + backgroundColor: t.palette.primary_500, 201 + color: t.palette.white, 202 + lineHeight: a.text_sm.fontSize, 203 + paddingHorizontal: 4, 204 + paddingVertical: 1, 205 + minWidth: 16, 247 206 }, 248 - ], 249 - ]}> 250 - {count} 251 - </Text> 207 + isTablet && [ 208 + { 209 + top: '10%', 210 + left: count.length === 1 ? 20 : 16, 211 + }, 212 + ], 213 + ]}> 214 + {count} 215 + </Text> 216 + </View> 252 217 ) : null} 253 218 </View> 254 219 {gtTablet && ( ··· 366 331 <View 367 332 role="navigation" 368 333 style={[ 334 + a.px_xl, 369 335 styles.leftNav, 370 336 isTablet && styles.leftNavTablet, 371 - pal.view, 372 337 pal.border, 373 338 ]}> 374 339 {hasSession ? ( ··· 381 346 382 347 {hasSession && ( 383 348 <> 384 - <BackBtn /> 385 - 386 349 <NavItem 387 350 href="/" 388 351 icon={ ··· 525 488 position: 'fixed', 526 489 top: 10, 527 490 // @ts-ignore web only 528 - left: 'calc(50vw - 300px - 220px - 20px)', 529 - width: 220, 491 + left: '50%', 492 + transform: [ 493 + { 494 + translateX: -300, 495 + }, 496 + { 497 + translateX: '-100%', 498 + }, 499 + ...a.scrollbar_offset.transform, 500 + ], 501 + width: 240, 530 502 // @ts-ignore web only 531 503 maxHeight: 'calc(100vh - 10px)', 532 504 overflowY: 'auto', ··· 538 510 borderRightWidth: 1, 539 511 height: '100%', 540 512 width: 76, 513 + paddingLeft: 0, 514 + paddingRight: 0, 541 515 alignItems: 'center', 516 + transform: [], 542 517 }, 543 518 544 519 profileCard: {
+8 -3
src/view/shell/desktop/RightNav.tsx
··· 28 28 } 29 29 30 30 return ( 31 - <View style={[styles.rightNav, pal.view]}> 31 + <View style={[a.px_xl, styles.rightNav]}> 32 32 <View style={{paddingVertical: 20}}> 33 33 {routeName === 'Search' ? ( 34 34 <View style={{marginBottom: 18}}> ··· 122 122 // @ts-ignore web only 123 123 position: 'fixed', 124 124 // @ts-ignore web only 125 - left: 'calc(50vw + 300px + 20px)', 126 - width: 300, 125 + left: '50%', 126 + transform: [ 127 + { 128 + translateX: 300, 129 + }, 130 + ...a.scrollbar_offset.transform, 131 + ], 127 132 maxHeight: '100%', 128 133 overflowY: 'auto', 129 134 },
+28 -26
src/view/shell/index.web.tsx
··· 3 3 import {msg} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 import {useNavigation} from '@react-navigation/native' 6 + import {RemoveScrollBar} from 'react-remove-scroll-bar' 6 7 7 8 import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' 8 9 import {useIntentHandler} from '#/lib/hooks/useIntentHandler' 9 - import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 10 10 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 11 11 import {NavigationProp} from '#/lib/routes/types' 12 12 import {colors} from '#/lib/styles' ··· 34 34 const {_} = useLingui() 35 35 const showDrawer = !isDesktop && isDrawerOpen 36 36 37 - useWebBodyScrollLock(showDrawer) 38 37 useComposerKeyboardShortcut() 39 38 useIntentHandler() 40 39 ··· 58 57 <PortalOutlet /> 59 58 60 59 {showDrawer && ( 61 - <TouchableWithoutFeedback 62 - onPress={ev => { 63 - // Only close if press happens outside of the drawer 64 - if (ev.target === ev.currentTarget) { 65 - setDrawerOpen(false) 66 - } 67 - }} 68 - accessibilityLabel={_(msg`Close navigation footer`)} 69 - accessibilityHint={_(msg`Closes bottom navigation bar`)}> 70 - <View 71 - style={[ 72 - styles.drawerMask, 73 - { 74 - backgroundColor: select(t.name, { 75 - light: 'rgba(0, 57, 117, 0.1)', 76 - dark: 'rgba(1, 82, 168, 0.1)', 77 - dim: 'rgba(10, 13, 16, 0.8)', 78 - }), 79 - }, 80 - ]}> 81 - <View style={styles.drawerContainer}> 82 - <DrawerContent /> 60 + <> 61 + <RemoveScrollBar /> 62 + <TouchableWithoutFeedback 63 + onPress={ev => { 64 + // Only close if press happens outside of the drawer 65 + if (ev.target === ev.currentTarget) { 66 + setDrawerOpen(false) 67 + } 68 + }} 69 + accessibilityLabel={_(msg`Close navigation footer`)} 70 + accessibilityHint={_(msg`Closes bottom navigation bar`)}> 71 + <View 72 + style={[ 73 + styles.drawerMask, 74 + { 75 + backgroundColor: select(t.name, { 76 + light: 'rgba(0, 57, 117, 0.1)', 77 + dark: 'rgba(1, 82, 168, 0.1)', 78 + dim: 'rgba(10, 13, 16, 0.8)', 79 + }), 80 + }, 81 + ]}> 82 + <View style={styles.drawerContainer}> 83 + <DrawerContent /> 84 + </View> 83 85 </View> 84 - </View> 85 - </TouchableWithoutFeedback> 86 + </TouchableWithoutFeedback> 87 + </> 86 88 )} 87 89 </> 88 90 )
+7 -2
web/index.html
··· 45 45 } 46 46 html { 47 47 background-color: white; 48 - scrollbar-gutter: stable both-edges; 49 48 } 50 49 @media (prefers-color-scheme: dark) { 51 50 html { ··· 81 80 top: 50%; 82 81 transform: translateX(-50%) translateY(-50%) translateY(-50px); 83 82 } 84 - /* We need this style to prevent web dropdowns from shifting the display when opening */ 83 + /** 84 + * We need these styles to prevent shifting due to scrollbar show/hide on 85 + * OSs that have them enabled by default. This also handles cases where the 86 + * screen wouldn't otherwise scroll, and therefore hide the scrollbar and 87 + * shift the content, by forcing the page to show a scrollbar. 88 + */ 85 89 body { 86 90 width: 100%; 91 + overflow-y: scroll; 87 92 } 88 93 </style> 89 94 </head>
+3 -28
yarn.lock
··· 17616 17616 resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" 17617 17617 integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== 17618 17618 17619 - "string-width-cjs@npm:string-width@^4.2.0": 17620 - version "4.2.3" 17621 - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" 17622 - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== 17623 - dependencies: 17624 - emoji-regex "^8.0.0" 17625 - is-fullwidth-code-point "^3.0.0" 17626 - strip-ansi "^6.0.1" 17627 - 17628 - string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: 17619 + "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: 17629 17620 version "4.2.3" 17630 17621 resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" 17631 17622 integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== ··· 17725 17716 dependencies: 17726 17717 safe-buffer "~5.1.0" 17727 17718 17728 - "strip-ansi-cjs@npm:strip-ansi@^6.0.1": 17719 + "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: 17729 17720 version "6.0.1" 17730 17721 resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" 17731 17722 integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== ··· 17738 17729 integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== 17739 17730 dependencies: 17740 17731 ansi-regex "^4.1.0" 17741 - 17742 - strip-ansi@^6.0.0, strip-ansi@^6.0.1: 17743 - version "6.0.1" 17744 - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" 17745 - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 17746 - dependencies: 17747 - ansi-regex "^5.0.1" 17748 17732 17749 17733 strip-ansi@^7.0.1: 17750 17734 version "7.1.0" ··· 19068 19052 resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" 19069 19053 integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== 19070 19054 19071 - "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": 19055 + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: 19072 19056 version "7.0.0" 19073 19057 resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" 19074 19058 integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== ··· 19081 19065 version "6.2.0" 19082 19066 resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" 19083 19067 integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== 19084 - dependencies: 19085 - ansi-styles "^4.0.0" 19086 - string-width "^4.1.0" 19087 - strip-ansi "^6.0.0" 19088 - 19089 - wrap-ansi@^7.0.0: 19090 - version "7.0.0" 19091 - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" 19092 - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== 19093 19068 dependencies: 19094 19069 ansi-styles "^4.0.0" 19095 19070 string-width "^4.1.0"