Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

[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

+1721 -2048
+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 } 41 html { 42 background-color: white; 43 - scrollbar-gutter: stable both-edges; 44 } 45 @media (prefers-color-scheme: dark) { 46 html { ··· 76 top: 50%; 77 transform: translateX(-50%) translateY(-50%) translateY(-50px); 78 } 79 - /* We need this style to prevent web dropdowns from shifting the display when opening */ 80 body { 81 width: 100%; 82 } 83 </style> 84
··· 40 } 41 html { 42 background-color: white; 43 } 44 @media (prefers-color-scheme: dark) { 45 html { ··· 75 top: 50%; 76 transform: translateX(-50%) translateY(-50%) translateY(-50px); 77 } 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 + */ 84 body { 85 width: 100%; 86 + overflow-y: scroll; 87 } 88 </style> 89
+1
package.json
··· 193 "react-native-web": "~0.19.11", 194 "react-native-web-webview": "^1.0.2", 195 "react-native-webview": "13.10.2", 196 "react-responsive": "^9.0.2", 197 "react-textarea-autosize": "^8.5.3", 198 "rn-fetch-blob": "^0.12.0",
··· 193 "react-native-web": "~0.19.11", 194 "react-native-web-webview": "^1.0.2", 195 "react-native-webview": "13.10.2", 196 + "react-remove-scroll-bar": "^2.3.6", 197 "react-responsive": "^9.0.2", 198 "react-textarea-autosize": "^8.5.3", 199 "rn-fetch-blob": "^0.12.0",
+21 -1
src/alf/atoms.ts
··· 1 import {Platform, StyleProp, StyleSheet, ViewStyle} from 'react-native' 2 3 import * as tokens from '#/alf/tokens' 4 - import {ios, native, web} from '#/alf/util/platform' 5 6 export const atoms = { 7 debug: { ··· 21 relative: { 22 position: 'relative', 23 }, 24 inset_0: { 25 top: 0, 26 left: 0, ··· 941 transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)', 942 transitionDuration: '100ms', 943 }), 944 } as const
··· 1 import {Platform, StyleProp, StyleSheet, ViewStyle} from 'react-native' 2 3 import * as tokens from '#/alf/tokens' 4 + import {ios, native, platform, web} from '#/alf/util/platform' 5 + import * as Layout from '#/components/Layout' 6 7 export const atoms = { 8 debug: { ··· 22 relative: { 23 position: 'relative', 24 }, 25 + sticky: web({ 26 + position: 'sticky', 27 + }), 28 inset_0: { 29 top: 0, 30 left: 0, ··· 945 transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)', 946 transitionDuration: '100ms', 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>}, 964 } as const
+1
src/alf/index.tsx
··· 20 export * from '#/alf/util/flatten' 21 export * from '#/alf/util/platform' 22 export * from '#/alf/util/themeSelector' 23 24 export type Alf = { 25 themeName: ThemeName
··· 20 export * from '#/alf/util/flatten' 21 export * from '#/alf/util/platform' 22 export * from '#/alf/util/themeSelector' 23 + export * from '#/alf/util/useGutterStyles' 24 25 export type Alf = { 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 import {DismissableLayer} from '@radix-ui/react-dismissable-layer' 13 import {useFocusGuards} from '@radix-ui/react-focus-guards' 14 import {FocusScope} from '@radix-ui/react-focus-scope' 15 16 import {logger} from '#/logger' 17 import {useDialogStateControlContext} from '#/state/dialogs' ··· 103 {isOpen && ( 104 <Portal> 105 <Context.Provider value={context}> 106 <TouchableWithoutFeedback 107 accessibilityHint={undefined} 108 accessibilityLabel={_(msg`Close active dialog`)}
··· 12 import {DismissableLayer} from '@radix-ui/react-dismissable-layer' 13 import {useFocusGuards} from '@radix-ui/react-focus-guards' 14 import {FocusScope} from '@radix-ui/react-focus-scope' 15 + import {RemoveScrollBar} from 'react-remove-scroll-bar' 16 17 import {logger} from '#/logger' 18 import {useDialogStateControlContext} from '#/state/dialogs' ··· 104 {isOpen && ( 105 <Portal> 106 <Context.Provider value={context}> 107 + <RemoveScrollBar /> 108 <TouchableWithoutFeedback 109 accessibilityHint={undefined} 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 import {List} from '#/view/com/util/List' 13 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 14 15 - function renderItem({item}: {item: GetLikes.Like}) { 16 - return <ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} /> 17 } 18 19 function keyExtractor(item: GetLikes.Like) { ··· 81 )} 82 errorMessage={cleanError(resolveError || error)} 83 onRetry={isError ? refetch : undefined} 84 /> 85 ) 86 } ··· 103 onEndReachedThreshold={3} 104 initialNumToRender={initialNumToRender} 105 windowSize={11} 106 /> 107 ) 108 }
··· 12 import {List} from '#/view/com/util/List' 13 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 14 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 + ) 23 } 24 25 function keyExtractor(item: GetLikes.Like) { ··· 87 )} 88 errorMessage={cleanError(resolveError || error)} 89 onRetry={isError ? refetch : undefined} 90 + topBorder={false} 91 + sideBorders={false} 92 /> 93 ) 94 } ··· 111 onEndReachedThreshold={3} 112 initialNumToRender={initialNumToRender} 113 windowSize={11} 114 + sideBorders={false} 115 /> 116 ) 117 }
+1 -33
src/components/Lists.tsx
··· 109 ) 110 } 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 let ListMaybePlaceholder = ({ 145 isLoading, 146 noEmpty, ··· 154 onGoBack, 155 hideBackButton, 156 sideBorders, 157 - topBorder = true, 158 }: { 159 isLoading: boolean 160 noEmpty?: boolean
··· 109 ) 110 } 111 112 let ListMaybePlaceholder = ({ 113 isLoading, 114 noEmpty, ··· 122 onGoBack, 123 hideBackButton, 124 sideBorders, 125 + topBorder = false, 126 }: { 127 isLoading: boolean 128 noEmpty?: boolean
+17 -19
src/components/dms/MessagesListHeader.tsx
··· 65 a.pr_lg, 66 a.py_sm, 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 - )} 87 88 {profile && moderation && blockInfo ? ( 89 <HeaderReady
··· 65 a.pr_lg, 66 a.py_sm, 67 ]}> 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> 85 86 {profile && moderation && blockInfo ? ( 87 <HeaderReady
+1
src/components/forms/DateField/index.android.tsx
··· 57 open 58 timeZoneOffsetInMinutes={0} 59 theme={t.scheme} 60 buttonColor={t.name === 'light' ? '#000000' : '#ffffff'} 61 date={new Date(value)} 62 onConfirm={onChangeInternal}
··· 57 open 58 timeZoneOffsetInMinutes={0} 59 theme={t.scheme} 60 + // @ts-ignore TODO 61 buttonColor={t.name === 'light' ? '#000000' : '#ffffff'} 62 date={new Date(value)} 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 } from '#/state/session' 18 import {useSetMinimalShellMode} from '#/state/shell' 19 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 20 - import {ScrollView} from '#/view/com/util/Views' 21 import {Logo} from '#/view/icons/Logo' 22 import {atoms as a, useTheme} from '#/alf' 23 import {AccountList} from '#/components/AccountList' 24 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 25 import {Divider} from '#/components/Divider' 26 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 27 import {Loader} from '#/components/Loader' 28 import {Text} from '#/components/Typography' 29 ··· 104 }, [_, agent, setPending, setError, queryClient]) 105 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, 112 a.px_2xl, 113 { 114 paddingTop: isWeb ? 64 : insets.top + 16, 115 paddingBottom: isWeb ? 64 : insets.bottom, 116 }, 117 - ]} 118 - contentContainerStyle={[ 119 - a.w_full, 120 - a.flex_row, 121 - a.justify_center, 122 - {borderWidth: 0}, 123 ]}> 124 - <View style={[a.w_full, {maxWidth: COL_WIDTH}]}> 125 <View style={[a.w_full, a.justify_center, a.align_center, a.pb_5xl]}> 126 <Logo width={40} /> 127 </View> ··· 218 </> 219 )} 220 </View> 221 - </ScrollView> 222 </View> 223 ) 224 }
··· 17 } from '#/state/session' 18 import {useSetMinimalShellMode} from '#/state/shell' 19 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 20 import {Logo} from '#/view/icons/Logo' 21 import {atoms as a, useTheme} from '#/alf' 22 import {AccountList} from '#/components/AccountList' 23 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 24 import {Divider} from '#/components/Divider' 25 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 26 + import * as Layout from '#/components/Layout' 27 import {Loader} from '#/components/Loader' 28 import {Text} from '#/components/Typography' 29 ··· 104 }, [_, agent, setPending, setError, queryClient]) 105 106 return ( 107 + <View style={[a.util_screen_outer, a.flex_1]}> 108 + <Layout.Content 109 + contentContainerStyle={[ 110 a.px_2xl, 111 { 112 paddingTop: isWeb ? 64 : insets.top + 16, 113 paddingBottom: isWeb ? 64 : insets.bottom, 114 }, 115 ]}> 116 + <View 117 + style={[a.w_full, {marginHorizontal: 'auto', maxWidth: COL_WIDTH}]}> 118 <View style={[a.w_full, a.justify_center, a.align_center, a.pb_5xl]}> 119 <Logo width={40} /> 120 </View> ··· 211 </> 212 )} 213 </View> 214 + </Layout.Content> 215 </View> 216 ) 217 }
+30 -41
src/screens/Hashtag.tsx
··· 1 import React from 'react' 2 - import {ListRenderItemInfo, Pressable, View} from 'react-native' 3 import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' ··· 13 import {cleanError} from '#/lib/strings/errors' 14 import {sanitizeHandle} from '#/lib/strings/handles' 15 import {enforceLen} from '#/lib/strings/helpers' 16 - import {isNative, isWeb} from '#/platform/detection' 17 import {useSearchPostsQuery} from '#/state/queries/search-posts' 18 import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell' 19 import {Pager} from '#/view/com/pager/Pager' 20 import {TabBar} from '#/view/com/pager/TabBar' 21 import {Post} from '#/view/com/post/Post' 22 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' 26 import * as Layout from '#/components/Layout' 27 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 28 ··· 110 111 return ( 112 <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> 136 <Pager 137 onPageSelected={onPageSelected} 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 - }> 151 <TabBar items={sections.map(section => section.title)} {...props} /> 152 - </CenteredView> 153 )} 154 initialPage={0}> 155 {sections.map((section, i) => (
··· 1 import React from 'react' 2 + import {ListRenderItemInfo, View} from 'react-native' 3 import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' ··· 13 import {cleanError} from '#/lib/strings/errors' 14 import {sanitizeHandle} from '#/lib/strings/handles' 15 import {enforceLen} from '#/lib/strings/helpers' 16 import {useSearchPostsQuery} from '#/state/queries/search-posts' 17 import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell' 18 import {Pager} from '#/view/com/pager/Pager' 19 import {TabBar} from '#/view/com/pager/TabBar' 20 import {Post} from '#/view/com/post/Post' 21 import {List} from '#/view/com/util/List' 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' 25 import * as Layout from '#/components/Layout' 26 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 27 ··· 109 110 return ( 111 <Layout.Screen> 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 <Pager 137 onPageSelected={onPageSelected} 138 renderTabBar={props => ( 139 + <Layout.Center style={web([a.sticky, a.z_10, {top: 0}])}> 140 <TabBar items={sections.map(section => section.title)} {...props} /> 141 + </Layout.Center> 142 )} 143 initialPage={0}> 144 {sections.map((section, i) => (
+55 -105
src/screens/Messages/ChatList.tsx
··· 16 import {useMessagesEventBus} from '#/state/messages/events' 17 import {useListConvosQuery} from '#/state/queries/messages/list-converations' 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 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 22 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 23 import {DialogControlProps, useDialogControl} from '#/components/Dialog' ··· 49 const {_} = useLingui() 50 const t = useTheme() 51 const newChatControl = useDialogControl() 52 - const {gtMobile} = useBreakpoints() 53 const pushToConversation = route.params?.pushToConversation 54 55 // Whenever we have `pushToConversation` set, it means we pressed a notification for a chat without being on ··· 81 }, [messagesBus, isActive]), 82 ) 83 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 const initialNumToRender = useInitialNumToRender({minItemHeight: 80}) 100 const [isPTRing, setIsPTRing] = useState(false) 101 ··· 144 [navigation], 145 ) 146 147 - const onNavigateToSettings = useCallback(() => { 148 - navigation.navigate('MessagesSettings') 149 - }, [navigation]) 150 - 151 if (conversations.length < 1) { 152 return ( 153 <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 - 169 {isLoading ? ( 170 <View style={[a.align_center, a.pt_3xl, web({paddingTop: '10vh'})]}> 171 <Loader size="xl" /> ··· 227 )} 228 </> 229 )} 230 - </CenteredView> 231 232 {!isLoading && !isError && ( 233 <NewChat onNewChat={onNewChat} control={newChatControl} /> ··· 238 239 return ( 240 <Layout.Screen testID="messagesScreen"> 241 - {!gtMobile && ( 242 - <ViewHeader 243 - title={_(msg`Messages`)} 244 - renderButton={renderButton} 245 - showBorder 246 - canGoBack={false} 247 - /> 248 - )} 249 <NewChat onNewChat={onNewChat} control={newChatControl} /> 250 <List 251 data={conversations} ··· 254 refreshing={isPTRing} 255 onRefresh={onRefresh} 256 onEndReached={onEndReached} 257 - ListHeaderComponent={ 258 - <DesktopHeader 259 - newChatControl={newChatControl} 260 - onNavigateToSettings={onNavigateToSettings} 261 - /> 262 - } 263 ListFooterComponent={ 264 <ListFooter 265 isFetchingNextPage={isFetchingNextPage} ··· 276 windowSize={11} 277 // @ts-ignore our .web version only -sfn 278 desktopFixedHeight 279 /> 280 </Layout.Screen> 281 ) 282 } 283 284 - function DesktopHeader({ 285 - newChatControl, 286 - onNavigateToSettings, 287 - }: { 288 - newChatControl: DialogControlProps 289 - onNavigateToSettings: () => void 290 - }) { 291 - const t = useTheme() 292 const {_} = useLingui() 293 - const {gtMobile, gtTablet} = useBreakpoints() 294 295 - if (!gtMobile) { 296 - return null 297 - } 298 299 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> 341 ) 342 }
··· 16 import {useMessagesEventBus} from '#/state/messages/events' 17 import {useListConvosQuery} from '#/state/queries/messages/list-converations' 18 import {List} from '#/view/com/util/List' 19 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 20 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 21 import {DialogControlProps, useDialogControl} from '#/components/Dialog' ··· 47 const {_} = useLingui() 48 const t = useTheme() 49 const newChatControl = useDialogControl() 50 const pushToConversation = route.params?.pushToConversation 51 52 // Whenever we have `pushToConversation` set, it means we pressed a notification for a chat without being on ··· 78 }, [messagesBus, isActive]), 79 ) 80 81 const initialNumToRender = useInitialNumToRender({minItemHeight: 80}) 82 const [isPTRing, setIsPTRing] = useState(false) 83 ··· 126 [navigation], 127 ) 128 129 if (conversations.length < 1) { 130 return ( 131 <Layout.Screen> 132 + <Header newChatControl={newChatControl} /> 133 + <Layout.Center> 134 {isLoading ? ( 135 <View style={[a.align_center, a.pt_3xl, web({paddingTop: '10vh'})]}> 136 <Loader size="xl" /> ··· 192 )} 193 </> 194 )} 195 + </Layout.Center> 196 197 {!isLoading && !isError && ( 198 <NewChat onNewChat={onNewChat} control={newChatControl} /> ··· 203 204 return ( 205 <Layout.Screen testID="messagesScreen"> 206 + <Header newChatControl={newChatControl} /> 207 <NewChat onNewChat={onNewChat} control={newChatControl} /> 208 <List 209 data={conversations} ··· 212 refreshing={isPTRing} 213 onRefresh={onRefresh} 214 onEndReached={onEndReached} 215 ListFooterComponent={ 216 <ListFooter 217 isFetchingNextPage={isFetchingNextPage} ··· 228 windowSize={11} 229 // @ts-ignore our .web version only -sfn 230 desktopFixedHeight 231 + sideBorders={false} 232 /> 233 </Layout.Screen> 234 ) 235 } 236 237 + function Header({newChatControl}: {newChatControl: DialogControlProps}) { 238 const {_} = useLingui() 239 + const {gtMobile} = useBreakpoints() 240 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 + ) 253 254 return ( 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> 291 ) 292 }
+4 -5
src/screens/Messages/Conversation.tsx
··· 17 import {useModerationOpts} from '#/state/preferences/moderation-opts' 18 import {useProfileQuery} from '#/state/queries/profile' 19 import {useSetMinimalShellMode} from '#/state/shell' 20 - import {CenteredView} from '#/view/com/util/Views' 21 import {MessagesList} from '#/screens/Messages/components/MessagesList' 22 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 23 import {useDialogControl} from '#/components/Dialog' ··· 97 98 if (convoState.status === ConvoStatus.Error) { 99 return ( 100 - <CenteredView style={[a.flex_1]} sideBorders> 101 <MessagesListHeader /> 102 <Error 103 title={_(msg`Something went wrong`)} ··· 105 onRetry={() => convoState.error.retry()} 106 sideBorders={false} 107 /> 108 - </CenteredView> 109 ) 110 } 111 112 return ( 113 - <CenteredView style={[a.flex_1]} sideBorders> 114 {!readyToShow && <MessagesListHeader />} 115 <View style={[a.flex_1]}> 116 {moderationOpts && recipient ? ( ··· 140 </View> 141 )} 142 </View> 143 - </CenteredView> 144 ) 145 } 146
··· 17 import {useModerationOpts} from '#/state/preferences/moderation-opts' 18 import {useProfileQuery} from '#/state/queries/profile' 19 import {useSetMinimalShellMode} from '#/state/shell' 20 import {MessagesList} from '#/screens/Messages/components/MessagesList' 21 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 22 import {useDialogControl} from '#/components/Dialog' ··· 96 97 if (convoState.status === ConvoStatus.Error) { 98 return ( 99 + <Layout.Center style={[a.flex_1]}> 100 <MessagesListHeader /> 101 <Error 102 title={_(msg`Something went wrong`)} ··· 104 onRetry={() => convoState.error.retry()} 105 sideBorders={false} 106 /> 107 + </Layout.Center> 108 ) 109 } 110 111 return ( 112 + <Layout.Center style={[a.flex_1]}> 113 {!readyToShow && <MessagesListHeader />} 114 <View style={[a.flex_1]}> 115 {moderationOpts && recipient ? ( ··· 139 </View> 140 )} 141 </View> 142 + </Layout.Center> 143 ) 144 } 145
+11 -5
src/screens/Messages/Settings.tsx
··· 10 import {useProfileQuery} from '#/state/queries/profile' 11 import {useSession} from '#/state/session' 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 import {atoms as a} from '#/alf' 16 import {Admonition} from '#/components/Admonition' 17 import {Divider} from '#/components/Divider' ··· 57 58 return ( 59 <Layout.Screen testID="messagesSettingsScreen"> 60 - <ScrollView stickyHeaderIndices={[0]}> 61 - <ViewHeader title={_(msg`Chat Settings`)} showOnDesktop showBorder /> 62 <View style={[a.p_lg, a.gap_md]}> 63 <Text style={[a.text_lg, a.font_bold]}> 64 <Trans>Allow new messages from</Trans> ··· 142 </> 143 )} 144 </View> 145 - </ScrollView> 146 </Layout.Screen> 147 ) 148 }
··· 10 import {useProfileQuery} from '#/state/queries/profile' 11 import {useSession} from '#/state/session' 12 import * as Toast from '#/view/com/util/Toast' 13 import {atoms as a} from '#/alf' 14 import {Admonition} from '#/components/Admonition' 15 import {Divider} from '#/components/Divider' ··· 55 56 return ( 57 <Layout.Screen testID="messagesSettingsScreen"> 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> 68 <View style={[a.p_lg, a.gap_md]}> 69 <Text style={[a.text_lg, a.font_bold]}> 70 <Trans>Allow new messages from</Trans> ··· 148 </> 149 )} 150 </View> 151 + </Layout.Content> 152 </Layout.Screen> 153 ) 154 }
+13 -34
src/screens/Moderation/index.tsx
··· 1 - import React from 'react' 2 import {Linking, View} from 'react-native' 3 - import {useSafeAreaFrame} from 'react-native-safe-area-context' 4 import {LABELS} from '@atproto/api' 5 import {msg, Trans} from '@lingui/macro' 6 import {useLingui} from '@lingui/react' ··· 19 import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities' 20 import {useSetMinimalShellMode} from '#/state/shell' 21 import {ViewHeader} from '#/view/com/util/ViewHeader' 22 - import {CenteredView} from '#/view/com/util/Views' 23 - import {ScrollView} from '#/view/com/util/Views' 24 import {atoms as a, useBreakpoints, useTheme, ViewStyleProp} from '#/alf' 25 import {Button, ButtonText} from '#/components/Button' 26 import * as Dialog from '#/components/Dialog' ··· 37 import * as LabelingService from '#/components/LabelingServiceCard' 38 import * as Layout from '#/components/Layout' 39 import {InlineLinkText, Link} from '#/components/Link' 40 import {Loader} from '#/components/Loader' 41 import {GlobalLabelPreference} from '#/components/moderation/LabelPreference' 42 import {Text} from '#/components/Typography' ··· 75 export function ModerationScreen( 76 _props: NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>, 77 ) { 78 - const t = useTheme() 79 const {_} = useLingui() 80 const { 81 isLoading: isPreferencesLoading, 82 error: preferencesError, 83 data: preferences, 84 } = usePreferencesQuery() 85 - const {gtMobile} = useBreakpoints() 86 - const {height} = useSafeAreaFrame() 87 88 const isLoading = isPreferencesLoading 89 const error = preferencesError 90 91 return ( 92 <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 - 103 {isLoading ? ( 104 - <View style={[a.w_full, a.align_center, a.pt_2xl]}> 105 - <Loader size="xl" fill={t.atoms.text.color} /> 106 - </View> 107 ) : error || !preferences ? ( 108 <ErrorState 109 error={ ··· 114 ) : ( 115 <ModerationScreenInner preferences={preferences} /> 116 )} 117 - </CenteredView> 118 </Layout.Screen> 119 ) 120 } ··· 169 } = useMyLabelersQuery() 170 171 useFocusEffect( 172 - React.useCallback(() => { 173 setMinimalShellMode(false) 174 }, [setMinimalShellMode]), 175 ) ··· 183 const ageNotSet = !preferences.userAge 184 const isUnderage = (preferences.userAge || 0) < 18 185 186 - const onToggleAdultContentEnabled = React.useCallback( 187 async (selected: boolean) => { 188 try { 189 await setAdultContentPref({ ··· 201 const disabledOnIOS = isIOS && !adultContentEnabled 202 203 return ( 204 - <ScrollView 205 - contentContainerStyle={[ 206 - a.border_0, 207 - a.pt_2xl, 208 - a.px_lg, 209 - gtMobile && a.px_2xl, 210 - ]}> 211 <Text 212 style={[a.text_md, a.font_bold, a.pb_md, t.atoms.text_contrast_high]}> 213 <Trans>Moderation tools</Trans> ··· 420 <View style={[a.rounded_sm, t.atoms.bg_contrast_25]}> 421 {labelers.map((labeler, i) => { 422 return ( 423 - <React.Fragment key={labeler.creator.did}> 424 {i !== 0 && <Divider />} 425 <LabelingService.Link labeler={labeler}> 426 {state => ( ··· 457 </LabelingService.Outer> 458 )} 459 </LabelingService.Link> 460 - </React.Fragment> 461 ) 462 })} 463 </View> 464 )} 465 - <View style={{height: 200}} /> 466 - </ScrollView> 467 ) 468 }
··· 1 + import {Fragment, useCallback} from 'react' 2 import {Linking, View} from 'react-native' 3 import {LABELS} from '@atproto/api' 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' ··· 18 import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities' 19 import {useSetMinimalShellMode} from '#/state/shell' 20 import {ViewHeader} from '#/view/com/util/ViewHeader' 21 import {atoms as a, useBreakpoints, useTheme, ViewStyleProp} from '#/alf' 22 import {Button, ButtonText} from '#/components/Button' 23 import * as Dialog from '#/components/Dialog' ··· 34 import * as LabelingService from '#/components/LabelingServiceCard' 35 import * as Layout from '#/components/Layout' 36 import {InlineLinkText, Link} from '#/components/Link' 37 + import {ListMaybePlaceholder} from '#/components/Lists' 38 import {Loader} from '#/components/Loader' 39 import {GlobalLabelPreference} from '#/components/moderation/LabelPreference' 40 import {Text} from '#/components/Typography' ··· 73 export function ModerationScreen( 74 _props: NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>, 75 ) { 76 const {_} = useLingui() 77 const { 78 isLoading: isPreferencesLoading, 79 error: preferencesError, 80 data: preferences, 81 } = usePreferencesQuery() 82 83 const isLoading = isPreferencesLoading 84 const error = preferencesError 85 86 return ( 87 <Layout.Screen testID="moderationScreen"> 88 + <ViewHeader title={_(msg`Moderation`)} showOnDesktop /> 89 + <Layout.Content> 90 {isLoading ? ( 91 + <ListMaybePlaceholder isLoading={true} sideBorders={false} /> 92 ) : error || !preferences ? ( 93 <ErrorState 94 error={ ··· 99 ) : ( 100 <ModerationScreenInner preferences={preferences} /> 101 )} 102 + </Layout.Content> 103 </Layout.Screen> 104 ) 105 } ··· 154 } = useMyLabelersQuery() 155 156 useFocusEffect( 157 + useCallback(() => { 158 setMinimalShellMode(false) 159 }, [setMinimalShellMode]), 160 ) ··· 168 const ageNotSet = !preferences.userAge 169 const isUnderage = (preferences.userAge || 0) < 18 170 171 + const onToggleAdultContentEnabled = useCallback( 172 async (selected: boolean) => { 173 try { 174 await setAdultContentPref({ ··· 186 const disabledOnIOS = isIOS && !adultContentEnabled 187 188 return ( 189 + <View style={[a.pt_2xl, a.px_lg, gtMobile && a.px_2xl]}> 190 <Text 191 style={[a.text_md, a.font_bold, a.pb_md, t.atoms.text_contrast_high]}> 192 <Trans>Moderation tools</Trans> ··· 399 <View style={[a.rounded_sm, t.atoms.bg_contrast_25]}> 400 {labelers.map((labeler, i) => { 401 return ( 402 + <Fragment key={labeler.creator.did}> 403 {i !== 0 && <Divider />} 404 <LabelingService.Link labeler={labeler}> 405 {state => ( ··· 436 </LabelingService.Outer> 437 )} 438 </LabelingService.Link> 439 + </Fragment> 440 ) 441 })} 442 </View> 443 )} 444 + <View style={{height: 150}} /> 445 + </View> 446 ) 447 }
+2 -4
src/screens/Onboarding/Layout.tsx
··· 1 import React from 'react' 2 - import {View} from 'react-native' 3 - import Animated from 'react-native-reanimated' 4 import {useSafeAreaInsets} from 'react-native-safe-area-context' 5 import {msg} from '@lingui/macro' 6 import {useLingui} from '@lingui/react' 7 8 import {isWeb} from '#/platform/detection' 9 import {useOnboardingDispatch} from '#/state/shell' 10 - import {ScrollView} from '#/view/com/util/Views' 11 import {Context} from '#/screens/Onboarding/state' 12 import { 13 atoms as a, ··· 36 const {gtMobile} = useBreakpoints() 37 const onboardDispatch = useOnboardingDispatch() 38 const {state, dispatch} = React.useContext(Context) 39 - const scrollview = React.useRef<Animated.ScrollView>(null) 40 const prevActiveStep = React.useRef<string>(state.activeStep) 41 42 React.useEffect(() => {
··· 1 import React from 'react' 2 + import {ScrollView, View} from 'react-native' 3 import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 import {isWeb} from '#/platform/detection' 8 import {useOnboardingDispatch} from '#/state/shell' 9 import {Context} from '#/screens/Onboarding/state' 10 import { 11 atoms as a, ··· 34 const {gtMobile} = useBreakpoints() 35 const onboardDispatch = useOnboardingDispatch() 36 const {state, dispatch} = React.useContext(Context) 37 + const scrollview = React.useRef<ScrollView>(null) 38 const prevActiveStep = React.useRef<string>(state.activeStep) 39 40 React.useEffect(() => {
+2 -8
src/screens/Post/PostLikedBy.tsx
··· 5 6 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 7 import {makeRecordUri} from '#/lib/strings/url-helpers' 8 - import {isWeb} from '#/platform/detection' 9 import {useSetMinimalShellMode} from '#/state/shell' 10 import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy' 11 import {ViewHeader} from '#/view/com/util/ViewHeader' 12 - import {CenteredView} from '#/view/com/util/Views' 13 import * as Layout from '#/components/Layout' 14 - import {ListHeaderDesktop} from '#/components/Lists' 15 16 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'> 17 export const PostLikedByScreen = ({route}: Props) => { ··· 28 29 return ( 30 <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> 36 </Layout.Screen> 37 ) 38 }
··· 5 6 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 7 import {makeRecordUri} from '#/lib/strings/url-helpers' 8 import {useSetMinimalShellMode} from '#/state/shell' 9 import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy' 10 import {ViewHeader} from '#/view/com/util/ViewHeader' 11 import * as Layout from '#/components/Layout' 12 13 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'> 14 export const PostLikedByScreen = ({route}: Props) => { ··· 25 26 return ( 27 <Layout.Screen> 28 + <ViewHeader title={_(msg`Liked By`)} /> 29 + <PostLikedByComponent uri={uri} /> 30 </Layout.Screen> 31 ) 32 }
-2
src/screens/Post/PostQuotes.tsx
··· 11 import {ViewHeader} from '#/view/com/util/ViewHeader' 12 import {CenteredView} from '#/view/com/util/Views' 13 import * as Layout from '#/components/Layout' 14 - import {ListHeaderDesktop} from '#/components/Lists' 15 16 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostQuotes'> 17 export const PostQuotesScreen = ({route}: Props) => { ··· 29 return ( 30 <Layout.Screen> 31 <CenteredView sideBorders={true}> 32 - <ListHeaderDesktop title={_(msg`Quotes`)} /> 33 <ViewHeader title={_(msg`Quotes`)} showBorder={!isWeb} /> 34 <PostQuotesComponent uri={uri} /> 35 </CenteredView>
··· 11 import {ViewHeader} from '#/view/com/util/ViewHeader' 12 import {CenteredView} from '#/view/com/util/Views' 13 import * as Layout from '#/components/Layout' 14 15 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostQuotes'> 16 export const PostQuotesScreen = ({route}: Props) => { ··· 28 return ( 29 <Layout.Screen> 30 <CenteredView sideBorders={true}> 31 <ViewHeader title={_(msg`Quotes`)} showBorder={!isWeb} /> 32 <PostQuotesComponent uri={uri} /> 33 </CenteredView>
-2
src/screens/Post/PostRepostedBy.tsx
··· 11 import {ViewHeader} from '#/view/com/util/ViewHeader' 12 import {CenteredView} from '#/view/com/util/Views' 13 import * as Layout from '#/components/Layout' 14 - import {ListHeaderDesktop} from '#/components/Lists' 15 16 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'> 17 export const PostRepostedByScreen = ({route}: Props) => { ··· 29 return ( 30 <Layout.Screen> 31 <CenteredView sideBorders={true}> 32 - <ListHeaderDesktop title={_(msg`Reposted By`)} /> 33 <ViewHeader title={_(msg`Reposted By`)} showBorder={!isWeb} /> 34 <PostRepostedByComponent uri={uri} /> 35 </CenteredView>
··· 11 import {ViewHeader} from '#/view/com/util/ViewHeader' 12 import {CenteredView} from '#/view/com/util/Views' 13 import * as Layout from '#/components/Layout' 14 15 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'> 16 export const PostRepostedByScreen = ({route}: Props) => { ··· 28 return ( 29 <Layout.Screen> 30 <CenteredView sideBorders={true}> 31 <ViewHeader title={_(msg`Reposted By`)} showBorder={!isWeb} /> 32 <PostRepostedByComponent uri={uri} /> 33 </CenteredView>
+19 -10
src/screens/Profile/KnownFollowers.tsx
··· 15 import {List} from '#/view/com/util/List' 16 import {ViewHeader} from '#/view/com/util/ViewHeader' 17 import * as Layout from '#/components/Layout' 18 - import { 19 - ListFooter, 20 - ListHeaderDesktop, 21 - ListMaybePlaceholder, 22 - } from '#/components/Lists' 23 24 - function renderItem({item}: {item: AppBskyActorDefs.ProfileViewBasic}) { 25 - return <ProfileCardWithFollowBtn key={item.did} profile={item} /> 26 } 27 28 function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) { ··· 93 if (followers.length < 1) { 94 return ( 95 <Layout.Screen> 96 <ListMaybePlaceholder 97 isLoading={isDidLoading || isFollowersLoading} 98 isError={isError} ··· 100 emptyMessage={_(msg`You don't follow any users who follow @${name}.`)} 101 errorMessage={cleanError(resolveError || error)} 102 onRetry={isError ? refetch : undefined} 103 /> 104 </Layout.Screen> 105 ) ··· 116 onRefresh={onRefresh} 117 onEndReached={onEndReached} 118 onEndReachedThreshold={4} 119 - ListHeaderComponent={ 120 - <ListHeaderDesktop title={_(msg`Followers you know`)} /> 121 - } 122 ListFooterComponent={ 123 <ListFooter 124 isFetchingNextPage={isFetchingNextPage} ··· 130 desktopFixedHeight 131 initialNumToRender={initialNumToRender} 132 windowSize={11} 133 /> 134 </Layout.Screen> 135 )
··· 15 import {List} from '#/view/com/util/List' 16 import {ViewHeader} from '#/view/com/util/ViewHeader' 17 import * as Layout from '#/components/Layout' 18 + import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 19 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 + ) 34 } 35 36 function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) { ··· 101 if (followers.length < 1) { 102 return ( 103 <Layout.Screen> 104 + <ViewHeader title={_(msg`Followers you know`)} /> 105 <ListMaybePlaceholder 106 isLoading={isDidLoading || isFollowersLoading} 107 isError={isError} ··· 109 emptyMessage={_(msg`You don't follow any users who follow @${name}.`)} 110 errorMessage={cleanError(resolveError || error)} 111 onRetry={isError ? refetch : undefined} 112 + topBorder={false} 113 + sideBorders={false} 114 /> 115 </Layout.Screen> 116 ) ··· 127 onRefresh={onRefresh} 128 onEndReached={onEndReached} 129 onEndReachedThreshold={4} 130 ListFooterComponent={ 131 <ListFooter 132 isFetchingNextPage={isFetchingNextPage} ··· 138 desktopFixedHeight 139 initialNumToRender={initialNumToRender} 140 windowSize={11} 141 + sideBorders={false} 142 /> 143 </Layout.Screen> 144 )
+4 -3
src/screens/Profile/Sections/Labels.tsx
··· 15 import {useScrollHandlers} from '#/lib/ScrollContext' 16 import {isNative} from '#/platform/detection' 17 import {ListRef} from '#/view/com/util/List' 18 - import {CenteredView, ScrollView} from '#/view/com/util/Views' 19 import {atoms as a, useTheme} from '#/alf' 20 import {Divider} from '#/components/Divider' 21 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 22 import {Loader} from '#/components/Loader' 23 import {LabelerLabelPreference} from '#/components/moderation/LabelPreference' 24 import {Text} from '#/components/Typography' ··· 75 }, [isFocused, scrollElRef, setScrollViewTag]) 76 77 return ( 78 - <CenteredView style={{flex: 1, minHeight}} sideBorders> 79 {isLabelerLoading ? ( 80 <View style={[a.w_full, a.align_center]}> 81 <Loader size="xl" /> ··· 95 headerHeight={headerHeight} 96 /> 97 )} 98 - </CenteredView> 99 ) 100 }) 101
··· 15 import {useScrollHandlers} from '#/lib/ScrollContext' 16 import {isNative} from '#/platform/detection' 17 import {ListRef} from '#/view/com/util/List' 18 + import {ScrollView} from '#/view/com/util/Views' 19 import {atoms as a, useTheme} from '#/alf' 20 import {Divider} from '#/components/Divider' 21 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 22 + import * as Layout from '#/components/Layout' 23 import {Loader} from '#/components/Loader' 24 import {LabelerLabelPreference} from '#/components/moderation/LabelPreference' 25 import {Text} from '#/components/Typography' ··· 76 }, [isFocused, scrollElRef, setScrollViewTag]) 77 78 return ( 79 + <Layout.Center style={{flex: 1, minHeight}}> 80 {isLabelerLoading ? ( 81 <View style={[a.w_full, a.align_center]}> 82 <Loader size="xl" /> ··· 96 headerHeight={headerHeight} 97 /> 98 )} 99 + </Layout.Center> 100 ) 101 }) 102
+9 -1
src/screens/Settings/AboutSettings.tsx
··· 21 22 return ( 23 <Layout.Screen> 24 - <Layout.Header title={_(msg`About`)} /> 25 <Layout.Content> 26 <SettingsList.Container> 27 <SettingsList.LinkItem
··· 21 22 return ( 23 <Layout.Screen> 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> 33 <Layout.Content> 34 <SettingsList.Container> 35 <SettingsList.LinkItem
+9 -1
src/screens/Settings/AccessibilitySettings.tsx
··· 39 40 return ( 41 <Layout.Screen> 42 - <Layout.Header title={_(msg`Accessibility`)} /> 43 <Layout.Content> 44 <SettingsList.Container> 45 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
··· 39 40 return ( 41 <Layout.Screen> 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> 51 <Layout.Content> 52 <SettingsList.Container> 53 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
+9 -1
src/screens/Settings/AccountSettings.tsx
··· 38 39 return ( 40 <Layout.Screen> 41 - <Layout.Header title={_(msg`Account`)} /> 42 <Layout.Content> 43 <SettingsList.Container> 44 <SettingsList.Item>
··· 38 39 return ( 40 <Layout.Screen> 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> 50 <Layout.Content> 51 <SettingsList.Container> 52 <SettingsList.Item>
+10 -2
src/screens/Settings/AppIconSettings.tsx
··· 1 import React from 'react' 2 import {Alert, View} from 'react-native' 3 import {Image} from 'expo-image' 4 - import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 import * as AppIcon from '@mozzius/expo-dynamic-app-icon' 7 import {NativeStackScreenProps} from '@react-navigation/native-stack' ··· 20 21 return ( 22 <Layout.Screen> 23 - <Layout.Header title={_('App Icon')} /> 24 <Layout.Content 25 contentContainerStyle={[a.py_2xl, a.px_xl, {paddingBottom: 100}]}> 26 <Text style={[a.text_lg, a.font_heavy]}>Defaults</Text>
··· 1 import React from 'react' 2 import {Alert, View} from 'react-native' 3 import {Image} from 'expo-image' 4 + import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 import * as AppIcon from '@mozzius/expo-dynamic-app-icon' 7 import {NativeStackScreenProps} from '@react-navigation/native-stack' ··· 20 21 return ( 22 <Layout.Screen> 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> 32 <Layout.Content 33 contentContainerStyle={[a.py_2xl, a.px_xl, {paddingBottom: 100}]}> 34 <Text style={[a.text_lg, a.font_heavy]}>Defaults</Text>
+9 -1
src/screens/Settings/AppPasswords.tsx
··· 44 45 return ( 46 <Layout.Screen testID="AppPasswordsScreen"> 47 - <Layout.Header title={_(msg`App Passwords`)} /> 48 <Layout.Content> 49 {error ? ( 50 <ErrorScreen
··· 44 45 return ( 46 <Layout.Screen testID="AppPasswordsScreen"> 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> 56 <Layout.Content> 57 {error ? ( 58 <ErrorScreen
+9 -1
src/screens/Settings/AppearanceSettings.tsx
··· 79 return ( 80 <LayoutAnimationConfig skipExiting skipEntering> 81 <Layout.Screen testID="preferencesThreadsScreen"> 82 - <Layout.Header title={_(msg`Appearance`)} /> 83 <Layout.Content> 84 <SettingsList.Container> 85 <AppearanceToggleButtonGroup
··· 79 return ( 80 <LayoutAnimationConfig skipExiting skipEntering> 81 <Layout.Screen testID="preferencesThreadsScreen"> 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> 91 <Layout.Content> 92 <SettingsList.Container> 93 <AppearanceToggleButtonGroup
+9 -1
src/screens/Settings/ContentAndMediaSettings.tsx
··· 32 33 return ( 34 <Layout.Screen> 35 - <Layout.Header title={_(msg`Content and Media`)} /> 36 <Layout.Content> 37 <SettingsList.Container> 38 <SettingsList.LinkItem
··· 32 33 return ( 34 <Layout.Screen> 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> 44 <Layout.Content> 45 <SettingsList.Container> 46 <SettingsList.LinkItem
+10 -4
src/screens/Settings/ExternalMediaPreferences.tsx
··· 1 import {Fragment} from 'react' 2 import {View} from 'react-native' 3 - import {msg, Trans} from '@lingui/macro' 4 - import {useLingui} from '@lingui/react' 5 6 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 7 import { ··· 23 'PreferencesExternalEmbeds' 24 > 25 export function ExternalMediaPreferencesScreen({}: Props) { 26 - const {_} = useLingui() 27 return ( 28 <Layout.Screen testID="externalMediaPreferencesScreen"> 29 - <Layout.Header title={_(msg`External Media Preferences`)} /> 30 <Layout.Content> 31 <SettingsList.Container> 32 <SettingsList.Item>
··· 1 import {Fragment} from 'react' 2 import {View} from 'react-native' 3 + import {Trans} from '@lingui/macro' 4 5 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 6 import { ··· 22 'PreferencesExternalEmbeds' 23 > 24 export function ExternalMediaPreferencesScreen({}: Props) { 25 return ( 26 <Layout.Screen testID="externalMediaPreferencesScreen"> 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> 36 <Layout.Content> 37 <SettingsList.Container> 38 <SettingsList.Item>
+9 -1
src/screens/Settings/FollowingFeedPreferences.tsx
··· 46 47 return ( 48 <Layout.Screen testID="followingFeedPreferencesScreen"> 49 - <Layout.Header title={_(msg`Following Feed Preferences`)} /> 50 <Layout.Content> 51 <SettingsList.Container> 52 <SettingsList.Item>
··· 46 47 return ( 48 <Layout.Screen testID="followingFeedPreferencesScreen"> 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> 58 <Layout.Content> 59 <SettingsList.Container> 60 <SettingsList.Item>
+9 -1
src/screens/Settings/LanguageSettings.tsx
··· 64 65 return ( 66 <Layout.Screen testID="PreferencesLanguagesScreen"> 67 - <Layout.Header title={_(msg`Languages`)} /> 68 <Layout.Content> 69 <SettingsList.Container> 70 <SettingsList.Group iconInset={false}>
··· 64 65 return ( 66 <Layout.Screen testID="PreferencesLanguagesScreen"> 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> 76 <Layout.Content> 77 <SettingsList.Container> 78 <SettingsList.Group iconInset={false}>
+9 -1
src/screens/Settings/NotificationSettings.tsx
··· 33 34 return ( 35 <Layout.Screen> 36 - <Layout.Header title={_(msg`Notification Settings`)} /> 37 <Layout.Content> 38 {isQueryError ? ( 39 <Error
··· 33 34 return ( 35 <Layout.Screen> 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> 45 <Layout.Content> 46 {isQueryError ? ( 47 <Error
+9 -1
src/screens/Settings/PrivacyAndSecuritySettings.tsx
··· 29 30 return ( 31 <Layout.Screen> 32 - <Layout.Header title={_(msg`Privacy and Security`)} /> 33 <Layout.Content> 34 <SettingsList.Container> 35 <SettingsList.Item>
··· 29 30 return ( 31 <Layout.Screen> 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> 41 <Layout.Content> 42 <SettingsList.Container> 43 <SettingsList.Item>
+9 -1
src/screens/Settings/Settings.tsx
··· 73 74 return ( 75 <Layout.Screen> 76 - <Layout.Header title={_(msg`Settings`)} /> 77 <Layout.Content> 78 <SettingsList.Container> 79 <View
··· 73 74 return ( 75 <Layout.Screen> 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> 85 <Layout.Content> 86 <SettingsList.Container> 87 <View
+9 -1
src/screens/Settings/ThreadPreferences.tsx
··· 38 39 return ( 40 <Layout.Screen testID="threadPreferencesScreen"> 41 - <Layout.Header title={_(msg`Thread Preferences`)} /> 42 <Layout.Content> 43 <SettingsList.Container> 44 <SettingsList.Group>
··· 38 39 return ( 40 <Layout.Screen testID="threadPreferencesScreen"> 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> 50 <Layout.Content> 51 <SettingsList.Container> 52 <SettingsList.Group>
+1 -2
src/screens/SignupQueued.tsx
··· 1 import React from 'react' 2 - import {Modal, View} from 'react-native' 3 import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 import {StatusBar} from 'expo-status-bar' 5 import {msg, plural, Trans} from '@lingui/macro' ··· 9 import {isIOS, isWeb} from '#/platform/detection' 10 import {isSignupQueued, useAgent, useSessionApi} from '#/state/session' 11 import {useOnboardingDispatch} from '#/state/shell' 12 - import {ScrollView} from '#/view/com/util/Views' 13 import {Logo} from '#/view/icons/Logo' 14 import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf' 15 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
··· 1 import React from 'react' 2 + import {Modal, ScrollView, View} from 'react-native' 3 import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 import {StatusBar} from 'expo-status-bar' 5 import {msg, plural, Trans} from '@lingui/macro' ··· 9 import {isIOS, isWeb} from '#/platform/detection' 10 import {isSignupQueued, useAgent, useSessionApi} from '#/state/session' 11 import {useOnboardingDispatch} from '#/state/shell' 12 import {Logo} from '#/view/icons/Logo' 13 import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf' 14 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+29 -52
src/screens/StarterPack/Wizard/index.tsx
··· 1 import React from 'react' 2 - import {Keyboard, TouchableOpacity, View} from 'react-native' 3 import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' 4 import {useSafeAreaInsets} from 'react-native-safe-area-context' 5 import {Image} from 'expo-image' ··· 10 ModerationOpts, 11 } from '@atproto/api' 12 import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' 13 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 14 import {msg, Plural, Trans} from '@lingui/macro' 15 import {useLingui} from '@lingui/react' 16 import {useFocusEffect, useNavigation} from '@react-navigation/native' 17 import {NativeStackScreenProps} from '@react-navigation/native-stack' 18 19 - import {HITSLOP_10, STARTER_PACK_MAX_SIZE} from '#/lib/constants' 20 import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' 21 import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' 22 import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' ··· 29 parseStarterPackUri, 30 } from '#/lib/strings/starter-pack' 31 import {logger} from '#/logger' 32 - import {isAndroid, isNative, isWeb} from '#/platform/detection' 33 import {useModerationOpts} from '#/state/preferences/moderation-opts' 34 import {useAllListMembersQuery} from '#/state/queries/list-members' 35 import {useProfileQuery} from '#/state/queries/profile' ··· 147 }) { 148 const navigation = useNavigation<NavigationProp>() 149 const {_} = useLingui() 150 - const t = useTheme() 151 const setMinimalShellMode = useSetMinimalShellMode() 152 const [state, dispatch] = useWizardState() 153 const {currentAccount} = useSession() ··· 283 284 return ( 285 <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> 325 326 <Container> 327 {state.currentStep === 'Details' ? ( ··· 463 <Trans> 464 <Text style={[a.font_bold, textStyles]}>You</Text> and 465 <Text> </Text> 466 - <Text style={[a.font_bold, textStyles]}> 467 {getName(items[1] /* [0] is self, skip it */)}{' '} 468 </Text> 469 are included in your starter pack 470 </Trans> 471 ) : items.length > 2 ? ( 472 <Trans context="profiles"> 473 - <Text style={[a.font_bold, textStyles]}> 474 {getName(items[1] /* [0] is self, skip it */)},{' '} 475 </Text> 476 - <Text style={[a.font_bold, textStyles]}> 477 {getName(items[2])},{' '} 478 </Text> 479 and{' '} ··· 504 { 505 items.length === 1 ? ( 506 <Trans> 507 - <Text style={[a.font_bold, textStyles]}> 508 {getName(items[0])} 509 </Text>{' '} 510 is included in your starter pack 511 </Trans> 512 ) : items.length === 2 ? ( 513 <Trans> 514 - <Text style={[a.font_bold, textStyles]}> 515 {getName(items[0])} 516 </Text>{' '} 517 and 518 <Text> </Text> 519 - <Text style={[a.font_bold, textStyles]}> 520 {getName(items[1])}{' '} 521 </Text> 522 are included in your starter pack 523 </Trans> 524 ) : items.length > 2 ? ( 525 <Trans context="feeds"> 526 - <Text style={[a.font_bold, textStyles]}> 527 {getName(items[0])},{' '} 528 </Text> 529 - <Text style={[a.font_bold, textStyles]}> 530 {getName(items[1])},{' '} 531 </Text> 532 and{' '}
··· 1 import React from 'react' 2 + import {Keyboard, View} from 'react-native' 3 import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' 4 import {useSafeAreaInsets} from 'react-native-safe-area-context' 5 import {Image} from 'expo-image' ··· 10 ModerationOpts, 11 } from '@atproto/api' 12 import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' 13 import {msg, Plural, Trans} from '@lingui/macro' 14 import {useLingui} from '@lingui/react' 15 import {useFocusEffect, useNavigation} from '@react-navigation/native' 16 import {NativeStackScreenProps} from '@react-navigation/native-stack' 17 18 + import {STARTER_PACK_MAX_SIZE} from '#/lib/constants' 19 import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' 20 import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' 21 import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' ··· 28 parseStarterPackUri, 29 } from '#/lib/strings/starter-pack' 30 import {logger} from '#/logger' 31 + import {isNative} from '#/platform/detection' 32 import {useModerationOpts} from '#/state/preferences/moderation-opts' 33 import {useAllListMembersQuery} from '#/state/queries/list-members' 34 import {useProfileQuery} from '#/state/queries/profile' ··· 146 }) { 147 const navigation = useNavigation<NavigationProp>() 148 const {_} = useLingui() 149 const setMinimalShellMode = useSetMinimalShellMode() 150 const [state, dispatch] = useWizardState() 151 const {currentAccount} = useSession() ··· 281 282 return ( 283 <CenteredView style={[a.flex_1]} sideBorders> 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> 302 303 <Container> 304 {state.currentStep === 'Details' ? ( ··· 440 <Trans> 441 <Text style={[a.font_bold, textStyles]}>You</Text> and 442 <Text> </Text> 443 + <Text style={[a.font_bold, textStyles]} emoji> 444 {getName(items[1] /* [0] is self, skip it */)}{' '} 445 </Text> 446 are included in your starter pack 447 </Trans> 448 ) : items.length > 2 ? ( 449 <Trans context="profiles"> 450 + <Text style={[a.font_bold, textStyles]} emoji> 451 {getName(items[1] /* [0] is self, skip it */)},{' '} 452 </Text> 453 + <Text style={[a.font_bold, textStyles]} emoji> 454 {getName(items[2])},{' '} 455 </Text> 456 and{' '} ··· 481 { 482 items.length === 1 ? ( 483 <Trans> 484 + <Text style={[a.font_bold, textStyles]} emoji> 485 {getName(items[0])} 486 </Text>{' '} 487 is included in your starter pack 488 </Trans> 489 ) : items.length === 2 ? ( 490 <Trans> 491 + <Text style={[a.font_bold, textStyles]} emoji> 492 {getName(items[0])} 493 </Text>{' '} 494 and 495 <Text> </Text> 496 + <Text style={[a.font_bold, textStyles]} emoji> 497 {getName(items[1])}{' '} 498 </Text> 499 are included in your starter pack 500 </Trans> 501 ) : items.length > 2 ? ( 502 <Trans context="feeds"> 503 + <Text style={[a.font_bold, textStyles]} emoji> 504 {getName(items[0])},{' '} 505 </Text> 506 + <Text style={[a.font_bold, textStyles]} emoji> 507 {getName(items[1])},{' '} 508 </Text> 509 and{' '}
+1 -1
src/view/com/feeds/FeedPage.tsx
··· 108 }, [scrollToTop, feed, queryClient, setHasNew]) 109 110 return ( 111 - <View testID={testID} style={s.h100pct}> 112 <MainScrollProvider> 113 <FeedFeedbackProvider value={feedFeedback}> 114 <Feed
··· 108 }, [scrollToTop, feed, queryClient, setHasNew]) 109 110 return ( 111 + <View testID={testID}> 112 <MainScrollProvider> 113 <FeedFeedbackProvider value={feedFeedback}> 114 <Feed
+36 -90
src/view/com/home/HomeHeaderLayout.web.tsx
··· 1 import React from 'react' 2 - import {StyleSheet, View} from 'react-native' 3 import Animated from 'react-native-reanimated' 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform' 8 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 9 import {useKawaiiMode} from '#/state/preferences/kawaii' 10 import {useSession} from '#/state/session' 11 import {useShellLayout} from '#/state/shell/shell-layout' 12 import {Logo} from '#/view/icons/Logo' 13 - import {atoms as a, useTheme} from '#/alf' 14 import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag' 15 import {Link} from '#/components/Link' 16 - import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile' 17 18 export function HomeHeaderLayout(props: { 19 children: React.ReactNode 20 tabBarAnchor: JSX.Element | null | undefined 21 }) { 22 - const {isMobile} = useWebMediaQueries() 23 - if (isMobile) { 24 return <HomeHeaderLayoutMobile {...props} /> 25 } else { 26 return <HomeHeaderLayoutDesktopAndTablet {...props} /> ··· 40 const {hasSession} = useSession() 41 const {_} = useLingui() 42 const kawaii = useKawaiiMode() 43 44 return ( 45 <> 46 {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 - ]}> 61 <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} /> 73 </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> 92 )} 93 {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> 107 </> 108 ) 109 } 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 - })
··· 1 import React from 'react' 2 + import {View} from 'react-native' 3 import Animated from 'react-native-reanimated' 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform' 8 import {useKawaiiMode} from '#/state/preferences/kawaii' 9 import {useSession} from '#/state/session' 10 import {useShellLayout} from '#/state/shell/shell-layout' 11 + import {HomeHeaderLayoutMobile} from '#/view/com/home/HomeHeaderLayoutMobile' 12 import {Logo} from '#/view/icons/Logo' 13 + import {atoms as a, useBreakpoints, useGutterStyles, useTheme} from '#/alf' 14 + import {ButtonIcon} from '#/components/Button' 15 import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag' 16 + import * as Layout from '#/components/Layout' 17 import {Link} from '#/components/Link' 18 19 export function HomeHeaderLayout(props: { 20 children: React.ReactNode 21 tabBarAnchor: JSX.Element | null | undefined 22 }) { 23 + const {gtMobile} = useBreakpoints() 24 + if (!gtMobile) { 25 return <HomeHeaderLayoutMobile {...props} /> 26 } else { 27 return <HomeHeaderLayoutDesktopAndTablet {...props} /> ··· 41 const {hasSession} = useSession() 42 const {_} = useLingui() 43 const kawaii = useKawaiiMode() 44 + const gutter = useGutterStyles() 45 46 return ( 47 <> 48 {hasSession && ( 49 + <Layout.Center> 50 <View 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> 67 </View> 68 + </Layout.Center> 69 )} 70 {tabBarAnchor} 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> 81 </> 82 ) 83 }
+44 -80
src/view/com/home/HomeHeaderLayoutMobile.tsx
··· 1 import React from 'react' 2 - import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 import Animated from 'react-native-reanimated' 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 import {HITSLOP_10} from '#/lib/constants' 8 import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform' 9 - import {usePalette} from '#/lib/hooks/usePalette' 10 - import {isWeb} from '#/platform/detection' 11 import {useSession} from '#/state/session' 12 - import {useSetDrawerOpen} from '#/state/shell/drawer-open' 13 import {useShellLayout} from '#/state/shell/shell-layout' 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' 19 import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag' 20 - import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' 21 import {Link} from '#/components/Link' 22 - import {IS_DEV} from '#/env' 23 24 export function HomeHeaderLayoutMobile({ 25 children, ··· 28 tabBarAnchor: JSX.Element | null | undefined 29 }) { 30 const t = useTheme() 31 - const pal = usePalette('default') 32 const {_} = useLingui() 33 - const setDrawerOpen = useSetDrawerOpen() 34 const {headerHeight} = useShellLayout() 35 const headerMinimalShellTransform = useMinimalShellHeaderTransform() 36 const {hasSession} = useSession() 37 - 38 - const onPressAvi = React.useCallback(() => { 39 - setDrawerOpen(true) 40 - }, [setDrawerOpen]) 41 42 return ( 43 <Animated.View 44 - style={[pal.border, styles.tabBar, headerMinimalShellTransform]} 45 onLayout={e => { 46 headerHeight.set(e.nativeEvent.layout.height) 47 }}> 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} /> 64 </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 - )} 83 {hasSession && ( 84 <Link 85 testID="viewHeaderHomeFeedPrefsBtn" ··· 93 style={[ 94 a.justify_center, 95 { 96 - marginTop: 2, 97 - marginRight: -6, 98 }, 99 ]}> 100 - <FeedsIcon size="lg" fill={t.atoms.text_contrast_medium.color} /> 101 </Link> 102 )} 103 - </View> 104 - </View> 105 {children} 106 </Animated.View> 107 ) 108 } 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 - })
··· 1 import React from 'react' 2 + import {View} from 'react-native' 3 import Animated from 'react-native-reanimated' 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 import {HITSLOP_10} from '#/lib/constants' 8 + import {PressableScale} from '#/lib/custom-animations/PressableScale' 9 + import {useHaptics} from '#/lib/haptics' 10 import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform' 11 + import {emitSoftReset} from '#/state/events' 12 import {useSession} from '#/state/session' 13 import {useShellLayout} from '#/state/shell/shell-layout' 14 import {Logo} from '#/view/icons/Logo' 15 + import {atoms as a, useTheme} from '#/alf' 16 + import {ButtonIcon} from '#/components/Button' 17 import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag' 18 + import * as Layout from '#/components/Layout' 19 import {Link} from '#/components/Link' 20 21 export function HomeHeaderLayoutMobile({ 22 children, ··· 25 tabBarAnchor: JSX.Element | null | undefined 26 }) { 27 const t = useTheme() 28 const {_} = useLingui() 29 const {headerHeight} = useShellLayout() 30 const headerMinimalShellTransform = useMinimalShellHeaderTransform() 31 const {hasSession} = useSession() 32 + const playHaptic = useHaptics() 33 34 return ( 35 <Animated.View 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 + ]} 47 onLayout={e => { 48 headerHeight.set(e.nativeEvent.layout.height) 49 }}> 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> 69 </View> 70 + 71 + <Layout.Header.Slot> 72 {hasSession && ( 73 <Link 74 testID="viewHeaderHomeFeedPrefsBtn" ··· 82 style={[ 83 a.justify_center, 84 { 85 + marginRight: -Layout.BUTTON_VISUAL_ALIGNMENT_OFFSET, 86 }, 87 ]}> 88 + <ButtonIcon icon={FeedsIcon} size="lg" /> 89 </Link> 90 )} 91 + </Layout.Header.Slot> 92 + </Layout.Header.Outer> 93 {children} 94 </Animated.View> 95 ) 96 }
+9 -7
src/view/com/lightbox/Lightbox.web.tsx
··· 15 } from '@fortawesome/react-native-fontawesome' 16 import {msg} from '@lingui/macro' 17 import {useLingui} from '@lingui/react' 18 19 - import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 20 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 21 import {colors, s} from '#/lib/styles' 22 import {useLightbox, useLightboxControls} from '#/state/lightbox' ··· 28 const {activeLightbox} = useLightbox() 29 const {closeLightbox} = useLightboxControls() 30 const isActive = !!activeLightbox 31 - useWebBodyScrollLock(isActive) 32 33 if (!isActive) { 34 return null ··· 37 const initialIndex = activeLightbox.index 38 const imgs = activeLightbox.images 39 return ( 40 - <LightboxInner 41 - imgs={imgs} 42 - initialIndex={initialIndex} 43 - onClose={closeLightbox} 44 - /> 45 ) 46 } 47
··· 15 } from '@fortawesome/react-native-fontawesome' 16 import {msg} from '@lingui/macro' 17 import {useLingui} from '@lingui/react' 18 + import {RemoveScrollBar} from 'react-remove-scroll-bar' 19 20 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 21 import {colors, s} from '#/lib/styles' 22 import {useLightbox, useLightboxControls} from '#/state/lightbox' ··· 28 const {activeLightbox} = useLightbox() 29 const {closeLightbox} = useLightboxControls() 30 const isActive = !!activeLightbox 31 32 if (!isActive) { 33 return null ··· 36 const initialIndex = activeLightbox.index 37 const imgs = activeLightbox.images 38 return ( 39 + <> 40 + <RemoveScrollBar /> 41 + <LightboxInner 42 + imgs={imgs} 43 + initialIndex={initialIndex} 44 + onClose={closeLightbox} 45 + /> 46 + </> 47 ) 48 } 49
+2 -5
src/view/com/lists/MyLists.tsx
··· 15 import {cleanError} from '#/lib/strings/errors' 16 import {s} from '#/lib/styles' 17 import {logger} from '#/logger' 18 - import {isWeb} from '#/platform/detection' 19 import {useModerationOpts} from '#/state/preferences/moderation-opts' 20 import {MyListsFilter, useMyListsQuery} from '#/state/queries/my-lists' 21 import {EmptyState} from '#/view/com/util/EmptyState' ··· 110 ) : ( 111 <View 112 style={[ 113 - (index !== 0 || isWeb) && a.border_t, 114 t.atoms.border_contrast_low, 115 a.px_lg, 116 a.py_lg, ··· 141 } 142 contentContainerStyle={[s.contentContainer]} 143 removeClippedSubviews={true} 144 - // @ts-ignore our .web version only -prf 145 - desktopFixedHeight 146 /> 147 )} 148 </View> ··· 160 onRefresh={onRefresh} 161 contentContainerStyle={[s.contentContainer]} 162 removeClippedSubviews={true} 163 - // @ts-ignore our .web version only -prf 164 desktopFixedHeight 165 /> 166 )} 167 </View>
··· 15 import {cleanError} from '#/lib/strings/errors' 16 import {s} from '#/lib/styles' 17 import {logger} from '#/logger' 18 import {useModerationOpts} from '#/state/preferences/moderation-opts' 19 import {MyListsFilter, useMyListsQuery} from '#/state/queries/my-lists' 20 import {EmptyState} from '#/view/com/util/EmptyState' ··· 109 ) : ( 110 <View 111 style={[ 112 + index !== 0 && a.border_t, 113 t.atoms.border_contrast_low, 114 a.px_lg, 115 a.py_lg, ··· 140 } 141 contentContainerStyle={[s.contentContainer]} 142 removeClippedSubviews={true} 143 /> 144 )} 145 </View> ··· 157 onRefresh={onRefresh} 158 contentContainerStyle={[s.contentContainer]} 159 removeClippedSubviews={true} 160 desktopFixedHeight 161 + sideBorders={false} 162 /> 163 )} 164 </View>
+2 -2
src/view/com/modals/Modal.web.tsx
··· 1 import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' 2 import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' 3 4 import {usePalette} from '#/lib/hooks/usePalette' 5 - import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 6 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 7 import type {Modal as ModalIface} from '#/state/modals' 8 import {useModalControls, useModals} from '#/state/modals' ··· 22 23 export function ModalsContainer() { 24 const {isModalActive, activeModals} = useModals() 25 - useWebBodyScrollLock(isModalActive) 26 27 if (!isModalActive) { 28 return null ··· 30 31 return ( 32 <> 33 {activeModals.map((modal, i) => ( 34 <Modal key={`modal-${i}`} modal={modal} /> 35 ))}
··· 1 import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' 2 import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' 3 + import {RemoveScrollBar} from 'react-remove-scroll-bar' 4 5 import {usePalette} from '#/lib/hooks/usePalette' 6 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 7 import type {Modal as ModalIface} from '#/state/modals' 8 import {useModalControls, useModals} from '#/state/modals' ··· 22 23 export function ModalsContainer() { 24 const {isModalActive, activeModals} = useModals() 25 26 if (!isModalActive) { 27 return null ··· 29 30 return ( 31 <> 32 + <RemoveScrollBar /> 33 {activeModals.map((modal, i) => ( 34 <Modal key={`modal-${i}`} modal={modal} /> 35 ))}
+7 -16
src/view/com/notifications/Feed.tsx
··· 10 11 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 12 import {usePalette} from '#/lib/hooks/usePalette' 13 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 14 import {cleanError} from '#/lib/strings/errors' 15 import {s} from '#/lib/styles' 16 import {logger} from '#/logger' ··· 22 import {List, ListRef} from '#/view/com/util/List' 23 import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 24 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 25 - import {CenteredView} from '#/view/com/util/Views' 26 import {FeedItem} from './FeedItem' 27 28 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} ··· 46 47 const [isPTRing, setIsPTRing] = React.useState(false) 48 const pal = usePalette('default') 49 - const {isTabletOrMobile} = useWebMediaQueries() 50 51 const {_} = useLingui() 52 const moderationOpts = useModerationOpts() ··· 133 ) 134 } else if (item === LOADING_ITEM) { 135 return ( 136 - <View 137 - style={[ 138 - pal.border, 139 - !isTabletOrMobile && {borderTopWidth: StyleSheet.hairlineWidth}, 140 - ]}> 141 <NotificationFeedLoadingPlaceholder /> 142 </View> 143 ) ··· 146 <FeedItem 147 item={item} 148 moderationOpts={moderationOpts!} 149 - hideTopBorder={index === 0 && isTabletOrMobile} 150 /> 151 ) 152 }, 153 - [moderationOpts, isTabletOrMobile, _, onPressRetryLoadMore, pal.border], 154 ) 155 156 const FeedFooter = React.useCallback( ··· 168 return ( 169 <View style={s.hContentRegion}> 170 {error && ( 171 - <CenteredView> 172 - <ErrorMessage 173 - message={cleanError(error)} 174 - onPressTryAgain={onPressTryAgain} 175 - /> 176 - </CenteredView> 177 )} 178 <List 179 testID="notifsFeed"
··· 10 11 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 12 import {usePalette} from '#/lib/hooks/usePalette' 13 import {cleanError} from '#/lib/strings/errors' 14 import {s} from '#/lib/styles' 15 import {logger} from '#/logger' ··· 21 import {List, ListRef} from '#/view/com/util/List' 22 import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 23 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 24 import {FeedItem} from './FeedItem' 25 26 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} ··· 44 45 const [isPTRing, setIsPTRing] = React.useState(false) 46 const pal = usePalette('default') 47 48 const {_} = useLingui() 49 const moderationOpts = useModerationOpts() ··· 130 ) 131 } else if (item === LOADING_ITEM) { 132 return ( 133 + <View style={[pal.border]}> 134 <NotificationFeedLoadingPlaceholder /> 135 </View> 136 ) ··· 139 <FeedItem 140 item={item} 141 moderationOpts={moderationOpts!} 142 + hideTopBorder={index === 0} 143 /> 144 ) 145 }, 146 + [moderationOpts, _, onPressRetryLoadMore, pal.border], 147 ) 148 149 const FeedFooter = React.useCallback( ··· 161 return ( 162 <View style={s.hContentRegion}> 163 {error && ( 164 + <ErrorMessage 165 + message={cleanError(error)} 166 + onPressTryAgain={onPressTryAgain} 167 + /> 168 )} 169 <List 170 testID="notifsFeed"
+11 -49
src/view/com/pager/PagerWithHeader.web.tsx
··· 1 import * as React from 'react' 2 - import {ScrollView, StyleSheet, View} from 'react-native' 3 import {useAnimatedRef} from 'react-native-reanimated' 4 5 - import {usePalette} from '#/lib/hooks/usePalette' 6 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 7 import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager' 8 import {ListMethods} from '../util/List' 9 import {TabBar} from './TabBar' 10 ··· 121 onSelect?: (index: number) => void 122 tabBarAnchor?: JSX.Element | null | undefined 123 }): React.ReactNode => { 124 - const pal = usePalette('default') 125 - const {isMobile} = useWebMediaQueries() 126 return ( 127 <> 128 - <View 129 - style={[ 130 - !isMobile && styles.headerContainerDesktop, 131 - pal.border, 132 - !isHeaderReady && styles.loadingHeader, 133 - ]}> 134 - {renderHeader?.()} 135 - </View> 136 {tabBarAnchor} 137 - <View 138 - style={[ 139 - styles.tabBarContainer, 140 - isMobile 141 - ? styles.tabBarContainerMobile 142 - : styles.tabBarContainerDesktop, 143 - pal.border, 144 { 145 display: isHeaderReady ? undefined : 'none', 146 }, 147 - ]}> 148 <TabBar 149 testID={testID} 150 items={items} ··· 154 dragProgress={undefined as any /* native-only */} 155 dragState={undefined as any /* native-only */} 156 /> 157 - </View> 158 </> 159 ) 160 } ··· 179 >, 180 }) 181 } 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 210 function toArray<T>(v: T | T[]): T[] { 211 if (Array.isArray(v)) {
··· 1 import * as React from 'react' 2 + import {ScrollView, View} from 'react-native' 3 import {useAnimatedRef} from 'react-native-reanimated' 4 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 import {ListMethods} from '../util/List' 9 import {TabBar} from './TabBar' 10 ··· 121 onSelect?: (index: number) => void 122 tabBarAnchor?: JSX.Element | null | undefined 123 }): React.ReactNode => { 124 return ( 125 <> 126 + <Layout.Center>{renderHeader?.()}</Layout.Center> 127 {tabBarAnchor} 128 + <Layout.Center 129 + style={web([ 130 + a.sticky, 131 + a.z_10, 132 { 133 + top: 0, 134 display: isHeaderReady ? undefined : 'none', 135 }, 136 + ])}> 137 <TabBar 138 testID={testID} 139 items={items} ··· 143 dragProgress={undefined as any /* native-only */} 144 dragState={undefined as any /* native-only */} 145 /> 146 + </Layout.Center> 147 </> 148 ) 149 } ··· 168 >, 169 }) 170 } 171 172 function toArray<T>(v: T | T[]): T[] { 173 if (Array.isArray(v)) {
+2 -3
src/view/com/post-thread/PostLikedBy.tsx
··· 6 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 7 import {cleanError} from '#/lib/strings/errors' 8 import {logger} from '#/logger' 9 - import {isWeb} from '#/platform/detection' 10 import {useLikedByQuery} from '#/state/queries/post-liked-by' 11 import {useResolveUriQuery} from '#/state/queries/resolve-uri' 12 import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' ··· 18 <ProfileCardWithFollowBtn 19 key={item.actor.did} 20 profile={item.actor} 21 - noBorder={index === 0 && !isWeb} 22 /> 23 ) 24 } ··· 88 )} 89 errorMessage={cleanError(resolveError || error)} 90 sideBorders={false} 91 /> 92 ) 93 } ··· 108 onRetry={fetchNextPage} 109 /> 110 } 111 - // @ts-ignore our .web version only -prf 112 desktopFixedHeight 113 initialNumToRender={initialNumToRender} 114 windowSize={11}
··· 6 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 7 import {cleanError} from '#/lib/strings/errors' 8 import {logger} from '#/logger' 9 import {useLikedByQuery} from '#/state/queries/post-liked-by' 10 import {useResolveUriQuery} from '#/state/queries/resolve-uri' 11 import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' ··· 17 <ProfileCardWithFollowBtn 18 key={item.actor.did} 19 profile={item.actor} 20 + noBorder={index === 0} 21 /> 22 ) 23 } ··· 87 )} 88 errorMessage={cleanError(resolveError || error)} 89 sideBorders={false} 90 + topBorder={false} 91 /> 92 ) 93 } ··· 108 onRetry={fetchNextPage} 109 /> 110 } 111 desktopFixedHeight 112 initialNumToRender={initialNumToRender} 113 windowSize={11}
+1 -2
src/view/com/post-thread/PostQuotes.tsx
··· 11 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' 12 import {cleanError} from '#/lib/strings/errors' 13 import {logger} from '#/logger' 14 - import {isWeb} from '#/platform/detection' 15 import {useModerationOpts} from '#/state/preferences/moderation-opts' 16 import {usePostQuotesQuery} from '#/state/queries/post-quotes' 17 import {useResolveUriQuery} from '#/state/queries/resolve-uri' ··· 30 } 31 index: number 32 }) { 33 - return <Post post={item.post} hideTopBorder={index === 0 && !isWeb} /> 34 } 35 36 function keyExtractor(item: {
··· 11 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' 12 import {cleanError} from '#/lib/strings/errors' 13 import {logger} from '#/logger' 14 import {useModerationOpts} from '#/state/preferences/moderation-opts' 15 import {usePostQuotesQuery} from '#/state/queries/post-quotes' 16 import {useResolveUriQuery} from '#/state/queries/resolve-uri' ··· 29 } 30 index: number 31 }) { 32 + return <Post post={item.post} hideTopBorder={index === 0} /> 33 } 34 35 function keyExtractor(item: {
+14 -2
src/view/com/post-thread/PostRepostedBy.tsx
··· 12 import {List} from '#/view/com/util/List' 13 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 14 15 - function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) { 16 - return <ProfileCardWithFollowBtn key={item.did} profile={item} /> 17 } 18 19 function keyExtractor(item: ActorDefs.ProfileViewBasic) {
··· 12 import {List} from '#/view/com/util/List' 13 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 14 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 + ) 29 } 30 31 function keyExtractor(item: ActorDefs.ProfileViewBasic) {
+2 -3
src/view/com/post-thread/PostThread.tsx
··· 32 import {useSession} from '#/state/session' 33 import {useComposerControls} from '#/state/shell' 34 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 35 - import {CenteredView} from '#/view/com/util/Views' 36 import {atoms as a, useTheme} from '#/alf' 37 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 38 import {Text} from '#/components/Typography' ··· 484 } 485 486 return ( 487 - <CenteredView style={[a.flex_1]} sideBorders={true}> 488 {showHeader && ( 489 <ViewHeader 490 title={_(msg({message: `Post`, context: 'description'}))} ··· 531 {isMobile && canReply && hasSession && ( 532 <MobileComposePrompt onPressReply={onPressReply} /> 533 )} 534 - </CenteredView> 535 ) 536 } 537
··· 32 import {useSession} from '#/state/session' 33 import {useComposerControls} from '#/state/shell' 34 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 35 import {atoms as a, useTheme} from '#/alf' 36 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 37 import {Text} from '#/components/Typography' ··· 483 } 484 485 return ( 486 + <> 487 {showHeader && ( 488 <ViewHeader 489 title={_(msg({message: `Post`, context: 'description'}))} ··· 530 {isMobile && canReply && hasSession && ( 531 <MobileComposePrompt onPressReply={onPressReply} /> 532 )} 533 + </> 534 ) 535 } 536
+1 -2
src/view/com/profile/ProfileFollowers.tsx
··· 6 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 7 import {cleanError} from '#/lib/strings/errors' 8 import {logger} from '#/logger' 9 - import {isWeb} from '#/platform/detection' 10 import {useProfileFollowersQuery} from '#/state/queries/profile-followers' 11 import {useResolveDidQuery} from '#/state/queries/resolve-uri' 12 import {useSession} from '#/state/session' ··· 25 <ProfileCardWithFollowBtn 26 key={item.did} 27 profile={item} 28 - noBorder={index === 0 && !isWeb} 29 /> 30 ) 31 }
··· 6 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 7 import {cleanError} from '#/lib/strings/errors' 8 import {logger} from '#/logger' 9 import {useProfileFollowersQuery} from '#/state/queries/profile-followers' 10 import {useResolveDidQuery} from '#/state/queries/resolve-uri' 11 import {useSession} from '#/state/session' ··· 24 <ProfileCardWithFollowBtn 25 key={item.did} 26 profile={item} 27 + noBorder={index === 0} 28 /> 29 ) 30 }
+1 -2
src/view/com/profile/ProfileFollows.tsx
··· 6 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 7 import {cleanError} from '#/lib/strings/errors' 8 import {logger} from '#/logger' 9 - import {isWeb} from '#/platform/detection' 10 import {useProfileFollowsQuery} from '#/state/queries/profile-follows' 11 import {useResolveDidQuery} from '#/state/queries/resolve-uri' 12 import {useSession} from '#/state/session' ··· 25 <ProfileCardWithFollowBtn 26 key={item.did} 27 profile={item} 28 - noBorder={index === 0 && !isWeb} 29 /> 30 ) 31 }
··· 6 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 7 import {cleanError} from '#/lib/strings/errors' 8 import {logger} from '#/logger' 9 import {useProfileFollowsQuery} from '#/state/queries/profile-follows' 10 import {useResolveDidQuery} from '#/state/queries/resolve-uri' 11 import {useSession} from '#/state/session' ··· 24 <ProfileCardWithFollowBtn 25 key={item.did} 26 profile={item} 27 + noBorder={index === 0} 28 /> 29 ) 30 }
+18 -85
src/view/com/profile/ProfileSubpageHeader.tsx
··· 1 import React from 'react' 2 - import {Pressable, StyleSheet, View} from 'react-native' 3 import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' 4 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 import {msg, Trans} from '@lingui/macro' 6 import {useLingui} from '@lingui/react' 7 import {useNavigation} from '@react-navigation/native' 8 9 - import {BACK_HITSLOP} from '#/lib/constants' 10 import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef' 11 import {usePalette} from '#/lib/hooks/usePalette' 12 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 13 import {makeProfileLink} from '#/lib/routes/links' 14 import {NavigationProp} from '#/lib/routes/types' 15 import {sanitizeHandle} from '#/lib/strings/handles' 16 - import {isNative} from '#/platform/detection' 17 import {emitSoftReset} from '#/state/events' 18 import {useLightboxControls} from '#/state/lightbox' 19 - import {useSetDrawerOpen} from '#/state/shell' 20 - import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' 21 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' 27 28 export function ProfileSubpageHeader({ 29 isLoading, ··· 48 | undefined 49 avatarType: UserAvatarType | 'starter-pack' 50 }>) { 51 - const setDrawerOpen = useSetDrawerOpen() 52 const navigation = useNavigation<NavigationProp>() 53 const {_} = useLingui() 54 const {isMobile} = useWebMediaQueries() ··· 57 const canGoBack = navigation.canGoBack() 58 const aviRef = useHandleRef() 59 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 const _openLightbox = React.useCallback( 73 (uri: string, thumbRect: MeasuredDimensions | null) => { 74 openLightbox({ ··· 106 }, [_openLightbox, avatar, aviRef]) 107 108 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 - )} 145 <View 146 style={{ 147 flexDirection: 'row', ··· 206 </Text> 207 )} 208 </View> 209 - {!isMobile && ( 210 - <View 211 - style={{ 212 - flexDirection: 'row', 213 - alignItems: 'center', 214 - }}> 215 - {children} 216 - </View> 217 - )} 218 </View> 219 - </CenteredView> 220 ) 221 } 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 - })
··· 1 import React from 'react' 2 + import {Pressable, View} from 'react-native' 3 import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 import {useNavigation} from '@react-navigation/native' 7 8 import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef' 9 import {usePalette} from '#/lib/hooks/usePalette' 10 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 11 import {makeProfileLink} from '#/lib/routes/links' 12 import {NavigationProp} from '#/lib/routes/types' 13 import {sanitizeHandle} from '#/lib/strings/handles' 14 import {emitSoftReset} from '#/state/events' 15 import {useLightboxControls} from '#/state/lightbox' 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' 20 import {StarterPack} from '#/components/icons/StarterPack' 21 + import * as Layout from '#/components/Layout' 22 23 export function ProfileSubpageHeader({ 24 isLoading, ··· 43 | undefined 44 avatarType: UserAvatarType | 'starter-pack' 45 }>) { 46 const navigation = useNavigation<NavigationProp>() 47 const {_} = useLingui() 48 const {isMobile} = useWebMediaQueries() ··· 51 const canGoBack = navigation.canGoBack() 52 const aviRef = useHandleRef() 53 54 const _openLightbox = React.useCallback( 55 (uri: string, thumbRect: MeasuredDimensions | null) => { 56 openLightbox({ ··· 88 }, [_openLightbox, avatar, aviRef]) 89 90 return ( 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 + 102 <View 103 style={{ 104 flexDirection: 'row', ··· 163 </Text> 164 )} 165 </View> 166 </View> 167 + </> 168 ) 169 }
+49 -66
src/view/com/util/List.web.tsx
··· 4 5 import {batchedUpdates} from '#/lib/batchedUpdates' 6 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 7 - import {usePalette} from '#/lib/hooks/usePalette' 8 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 9 import {useScrollHandlers} from '#/lib/ScrollContext' 10 import {addStyle} from '#/lib/styles' 11 12 export type ListMethods = any // TODO: Better types. 13 export type ListProps<ItemT> = Omit< ··· 24 desktopFixedHeight?: number | boolean 25 // Web only prop to contain the scroll to the container rather than the window 26 disableFullWindowScroll?: boolean 27 sideBorders?: boolean 28 } 29 export type ListRef = React.MutableRefObject<any | null> // TODO: Better types. ··· 56 renderItem, 57 extraData, 58 style, 59 - sideBorders = true, 60 ...props 61 }: ListProps<ItemT>, 62 ref: React.Ref<ListMethods>, 63 ) { 64 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 74 const isEmpty = !data || data.length === 0 75 ··· 326 styles.parentTreeVisibilityDetector 327 } 328 /> 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 344 root={disableFullWindowScroll ? nativeRef : null} 345 - onVisibleChange={onHeadVisibilityChange} 346 - topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'} 347 - containerRef={containerRef} 348 /> 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> 376 </View> 377 ) 378 } ··· 558 // https://stackoverflow.com/questions/7944460/detect-safari-browser 559 560 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 minHeightViewport: { 572 // @ts-ignore web only 573 minHeight: '100vh',
··· 4 5 import {batchedUpdates} from '#/lib/batchedUpdates' 6 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 7 import {useScrollHandlers} from '#/lib/ScrollContext' 8 import {addStyle} from '#/lib/styles' 9 + import * as Layout from '#/components/Layout' 10 11 export type ListMethods = any // TODO: Better types. 12 export type ListProps<ItemT> = Omit< ··· 23 desktopFixedHeight?: number | boolean 24 // Web only prop to contain the scroll to the container rather than the window 25 disableFullWindowScroll?: boolean 26 + /** 27 + * @deprecated Should be using Layout components 28 + */ 29 sideBorders?: boolean 30 } 31 export type ListRef = React.MutableRefObject<any | null> // TODO: Better types. ··· 58 renderItem, 59 extraData, 60 style, 61 ...props 62 }: ListProps<ItemT>, 63 ref: React.Ref<ListMethods>, 64 ) { 65 const contextScrollHandlers = useScrollHandlers() 66 67 const isEmpty = !data || data.length === 0 68 ··· 319 styles.parentTreeVisibilityDetector 320 } 321 /> 322 + <Layout.Center> 323 + <View 324 + ref={containerRef} 325 + style={[ 326 + contentContainerStyle, 327 + desktopFixedHeight ? styles.minHeightViewport : null, 328 + ]}> 329 + <Visibility 330 root={disableFullWindowScroll ? nativeRef : null} 331 + onVisibleChange={handleAboveTheFoldVisibleChange} 332 + style={[styles.aboveTheFoldDetector, {height: headerOffset}]} 333 /> 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> 369 </View> 370 ) 371 } ··· 551 // https://stackoverflow.com/questions/7944460/detect-safari-browser 552 553 const styles = StyleSheet.create({ 554 minHeightViewport: { 555 // @ts-ignore web only 556 minHeight: '100vh',
+6 -3
src/view/com/util/LoadingScreen.tsx
··· 1 import {ActivityIndicator, View} from 'react-native' 2 3 import {s} from '#/lib/styles' 4 - import {CenteredView} from './Views' 5 6 export function LoadingScreen() { 7 return ( 8 - <CenteredView> 9 <View style={s.p20}> 10 <ActivityIndicator size="large" /> 11 </View> 12 - </CenteredView> 13 ) 14 }
··· 1 import {ActivityIndicator, View} from 'react-native' 2 3 import {s} from '#/lib/styles' 4 + import * as Layout from '#/components/Layout' 5 6 + /** 7 + * @deprecated use Layout compoenents directly 8 + */ 9 export function LoadingScreen() { 10 return ( 11 + <Layout.Content> 12 <View style={s.p20}> 13 <ActivityIndicator size="large" /> 14 </View> 15 + </Layout.Content> 16 ) 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' 18 19 - const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} 20 - 21 export function ViewHeader({ 22 title, 23 - subtitle, 24 - canGoBack, 25 - showBackButton = true, 26 - hideOnScroll, 27 - showOnDesktop, 28 - showBorder, 29 renderButton, 30 }: { 31 title: string 32 subtitle?: string 33 - canGoBack?: boolean 34 - showBackButton?: boolean 35 - hideOnScroll?: boolean 36 showOnDesktop?: boolean 37 showBorder?: boolean 38 renderButton?: () => JSX.Element 39 }) { 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 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> 180 ) 181 } 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 - })
··· 1 + import {Header} from '#/components/Layout' 2 3 + /** 4 + * Legacy ViewHeader component. Use Layout.Header going forward. 5 + * 6 + * @deprecated 7 + */ 8 export function ViewHeader({ 9 title, 10 renderButton, 11 }: { 12 title: string 13 subtitle?: string 14 showOnDesktop?: boolean 15 showBorder?: boolean 16 renderButton?: () => JSX.Element 17 }) { 18 return ( 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> 26 ) 27 }
+7
src/view/com/util/Views.tsx
··· 15 FlatListComponent<ItemT, FlatListPropsWithLayout<ItemT>>, 16 'CellRendererComponent' 17 > 18 export const ScrollView = Animated.ScrollView 19 export type ScrollView = typeof Animated.ScrollView 20 21 export const CenteredView = forwardRef< 22 View, 23 React.PropsWithChildren<
··· 15 FlatListComponent<ItemT, FlatListPropsWithLayout<ItemT>>, 16 'CellRendererComponent' 17 > 18 + 19 + /** 20 + * @deprecated use `Layout` components 21 + */ 22 export const ScrollView = Animated.ScrollView 23 export type ScrollView = typeof Animated.ScrollView 24 25 + /** 26 + * @deprecated use `Layout` components 27 + */ 28 export const CenteredView = forwardRef< 29 View, 30 React.PropsWithChildren<
+8 -23
src/view/com/util/Views.web.tsx
··· 31 desktopFixedHeight?: boolean | number 32 } 33 34 export const CenteredView = React.forwardRef(function CenteredView( 35 { 36 style, 37 - sideBorders, 38 topBorder, 39 ...props 40 }: React.PropsWithChildren< ··· 47 if (!isMobile) { 48 style = addStyle(style, styles.container) 49 } 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 if (topBorder) { 58 style = addStyle(style, { 59 borderTopWidth: 1, ··· 75 >, 76 ref: React.Ref<FlatList<ItemT>>, 77 ) { 78 - const pal = usePalette('default') 79 const {isMobile} = useWebMediaQueries() 80 if (!isMobile) { 81 contentContainerStyle = addStyle( ··· 123 return ( 124 <Animated.FlatList 125 ref={ref} 126 - contentContainerStyle={[ 127 - styles.contentContainer, 128 - contentContainerStyle, 129 - pal.border, 130 - ]} 131 style={style} 132 contentOffset={contentOffset} 133 {...props} ··· 135 ) 136 }) 137 138 export const ScrollView = React.forwardRef(function ScrollViewImpl( 139 {contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>, 140 ref: React.Ref<Animated.ScrollView>, 141 ) { 142 - const pal = usePalette('default') 143 - 144 const {isMobile} = useWebMediaQueries() 145 if (!isMobile) { 146 contentContainerStyle = addStyle( ··· 150 } 151 return ( 152 <Animated.ScrollView 153 - contentContainerStyle={[ 154 - styles.contentContainer, 155 - contentContainerStyle, 156 - pal.border, 157 - ]} 158 // @ts-ignore something is wrong with the reanimated types -prf 159 ref={ref} 160 {...props} ··· 164 165 const styles = StyleSheet.create({ 166 contentContainer: { 167 - borderLeftWidth: StyleSheet.hairlineWidth, 168 - borderRightWidth: StyleSheet.hairlineWidth, 169 // @ts-ignore web only 170 minHeight: '100vh', 171 },
··· 31 desktopFixedHeight?: boolean | number 32 } 33 34 + /** 35 + * @deprecated use `Layout` components 36 + */ 37 export const CenteredView = React.forwardRef(function CenteredView( 38 { 39 style, 40 topBorder, 41 ...props 42 }: React.PropsWithChildren< ··· 49 if (!isMobile) { 50 style = addStyle(style, styles.container) 51 } 52 if (topBorder) { 53 style = addStyle(style, { 54 borderTopWidth: 1, ··· 70 >, 71 ref: React.Ref<FlatList<ItemT>>, 72 ) { 73 const {isMobile} = useWebMediaQueries() 74 if (!isMobile) { 75 contentContainerStyle = addStyle( ··· 117 return ( 118 <Animated.FlatList 119 ref={ref} 120 + contentContainerStyle={[styles.contentContainer, contentContainerStyle]} 121 style={style} 122 contentOffset={contentOffset} 123 {...props} ··· 125 ) 126 }) 127 128 + /** 129 + * @deprecated use `Layout` components 130 + */ 131 export const ScrollView = React.forwardRef(function ScrollViewImpl( 132 {contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>, 133 ref: React.Ref<Animated.ScrollView>, 134 ) { 135 const {isMobile} = useWebMediaQueries() 136 if (!isMobile) { 137 contentContainerStyle = addStyle( ··· 141 } 142 return ( 143 <Animated.ScrollView 144 + contentContainerStyle={[styles.contentContainer, contentContainerStyle]} 145 // @ts-ignore something is wrong with the reanimated types -prf 146 ref={ref} 147 {...props} ··· 151 152 const styles = StyleSheet.create({ 153 contentContainer: { 154 // @ts-ignore web only 155 minHeight: '100vh', 156 },
+3 -1
src/view/com/util/error/ErrorScreen.tsx
··· 36 37 return ( 38 <> 39 - {showHeader && isMobile && <ViewHeader title="Error" showBorder />} 40 <CenteredView testID={testID} style={[styles.outer, pal.view]}> 41 <View style={styles.errorIconContainer}> 42 <View
··· 36 37 return ( 38 <> 39 + {showHeader && isMobile && ( 40 + <ViewHeader title={_(msg`Error`)} showBorder /> 41 + )} 42 <CenteredView testID={testID} style={[styles.outer, pal.view]}> 43 <View style={styles.errorIconContainer}> 44 <View
+45 -83
src/view/screens/Feeds.tsx
··· 24 import {useComposerControls} from '#/state/shell/composer' 25 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 26 import {FAB} from '#/view/com/util/fab/FAB' 27 - import {TextLink} from '#/view/com/util/Link' 28 import {List, ListMethods} from '#/view/com/util/List' 29 import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 30 import {Text} from '#/view/com/util/text/Text' 31 - import {ViewHeader} from '#/view/com/util/ViewHeader' 32 import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed' 33 import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType' 34 import {atoms as a, useTheme} from '#/alf' 35 import {Divider} from '#/components/Divider' 36 import * as FeedCard from '#/components/FeedCard' 37 import {SearchInput} from '#/components/forms/SearchInput' ··· 40 import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' 41 import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass' 42 import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle' 43 import * as Layout from '#/components/Layout' 44 import * as ListCard from '#/components/ListCard' 45 46 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'> ··· 102 export function FeedsScreen(_props: Props) { 103 const pal = usePalette('default') 104 const {openComposer} = useComposerControls() 105 - const {isMobile, isTabletOrDesktop} = useWebMediaQueries() 106 const [query, setQuery] = React.useState('') 107 const [isPTR, setIsPTR] = React.useState(false) 108 const { ··· 374 isUserSearching, 375 ]) 376 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 const searchBarIndex = items.findIndex( 394 item => item.type === 'popularFeedsHeader', 395 ) ··· 430 </View> 431 ) 432 } 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 - ) 463 } else if (item.type === 'savedFeedNoResults') { 464 return ( 465 <View ··· 530 return null 531 }, 532 [ 533 - isMobile, 534 - pal.view, 535 pal.border, 536 - pal.text, 537 pal.textLight, 538 - pal.link, 539 - _, 540 query, 541 onChangeQuery, 542 onPressCancelSearch, ··· 547 548 return ( 549 <Layout.Screen testID="FeedsScreen"> 550 - {isMobile && ( 551 - <ViewHeader 552 - title={_(msg`Feeds`)} 553 - renderButton={renderHeaderBtn} 554 - showBorder 555 /> 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 - /> 575 576 {hasSession && ( 577 <FAB ··· 728 }> 729 <IconCircle icon={ListSparkle_Stroke2_Corner0_Rounded} size="lg" /> 730 <View style={[a.flex_1, a.gap_xs]}> 731 - <Text style={[a.flex_1, a.text_2xl, a.font_bold, t.atoms.text]}> 732 <Trans>My Feeds</Trans> 733 </Text> 734 <Text style={[t.atoms.text_contrast_high]}> ··· 754 size="lg" 755 /> 756 <View style={[a.flex_1, a.gap_sm]}> 757 - <Text style={[a.flex_1, a.text_2xl, a.font_bold, t.atoms.text]}> 758 <Trans>Discover New Feeds</Trans> 759 </Text> 760 <Text style={[t.atoms.text_contrast_high]}> ··· 769 } 770 771 const styles = StyleSheet.create({ 772 - list: { 773 - height: '100%', 774 - }, 775 contentContainer: { 776 paddingBottom: 100, 777 },
··· 24 import {useComposerControls} from '#/state/shell/composer' 25 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 26 import {FAB} from '#/view/com/util/fab/FAB' 27 import {List, ListMethods} from '#/view/com/util/List' 28 import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 29 import {Text} from '#/view/com/util/text/Text' 30 import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed' 31 import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType' 32 import {atoms as a, useTheme} from '#/alf' 33 + import {ButtonIcon} from '#/components/Button' 34 import {Divider} from '#/components/Divider' 35 import * as FeedCard from '#/components/FeedCard' 36 import {SearchInput} from '#/components/forms/SearchInput' ··· 39 import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' 40 import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass' 41 import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle' 42 + import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2' 43 import * as Layout from '#/components/Layout' 44 + import {Link} from '#/components/Link' 45 import * as ListCard from '#/components/ListCard' 46 47 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'> ··· 103 export function FeedsScreen(_props: Props) { 104 const pal = usePalette('default') 105 const {openComposer} = useComposerControls() 106 + const {isMobile} = useWebMediaQueries() 107 const [query, setQuery] = React.useState('') 108 const [isPTR, setIsPTR] = React.useState(false) 109 const { ··· 375 isUserSearching, 376 ]) 377 378 const searchBarIndex = items.findIndex( 379 item => item.type === 'popularFeedsHeader', 380 ) ··· 415 </View> 416 ) 417 } else if (item.type === 'savedFeedsHeader') { 418 + return <FeedsSavedHeader /> 419 } else if (item.type === 'savedFeedNoResults') { 420 return ( 421 <View ··· 486 return null 487 }, 488 [ 489 pal.border, 490 pal.textLight, 491 query, 492 onChangeQuery, 493 onPressCancelSearch, ··· 498 499 return ( 500 <Layout.Screen testID="FeedsScreen"> 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} 538 /> 539 + </Layout.Center> 540 541 {hasSession && ( 542 <FAB ··· 693 }> 694 <IconCircle icon={ListSparkle_Stroke2_Corner0_Rounded} size="lg" /> 695 <View style={[a.flex_1, a.gap_xs]}> 696 + <Text style={[a.flex_1, a.text_2xl, a.font_heavy, t.atoms.text]}> 697 <Trans>My Feeds</Trans> 698 </Text> 699 <Text style={[t.atoms.text_contrast_high]}> ··· 719 size="lg" 720 /> 721 <View style={[a.flex_1, a.gap_sm]}> 722 + <Text style={[a.flex_1, a.text_2xl, a.font_heavy, t.atoms.text]}> 723 <Trans>Discover New Feeds</Trans> 724 </Text> 725 <Text style={[t.atoms.text_contrast_high]}> ··· 734 } 735 736 const styles = StyleSheet.create({ 737 contentContainer: { 738 paddingBottom: 100, 739 },
+26 -46
src/view/screens/Lists.tsx
··· 1 import React from 'react' 2 - import {StyleSheet, View} from 'react-native' 3 import {AtUri} from '@atproto/api' 4 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 import {msg, Trans} from '@lingui/macro' 6 import {useLingui} from '@lingui/react' 7 import {useFocusEffect, useNavigation} from '@react-navigation/native' 8 9 import {useEmail} from '#/lib/hooks/useEmail' 10 - import {usePalette} from '#/lib/hooks/usePalette' 11 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 12 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 13 import {NavigationProp} from '#/lib/routes/types' 14 - import {s} from '#/lib/styles' 15 import {useModalControls} from '#/state/modals' 16 import {useSetMinimalShellMode} from '#/state/shell' 17 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' 21 import {useDialogControl} from '#/components/Dialog' 22 import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 23 import * as Layout from '#/components/Layout' 24 25 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'> 26 export function ListsScreen({}: Props) { 27 const {_} = useLingui() 28 - const pal = usePalette('default') 29 const setMinimalShellMode = useSetMinimalShellMode() 30 - const {isMobile} = useWebMediaQueries() 31 const navigation = useNavigation<NavigationProp>() 32 const {openModal} = useModalControls() 33 const {needsEmailVerification} = useEmail() ··· 62 63 return ( 64 <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}> 81 <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} /> 102 <VerifyEmailDialog 103 reasonText={_( 104 msg`Before creating a list, you must first verify your email.`,
··· 1 import React from 'react' 2 import {AtUri} from '@atproto/api' 3 import {msg, Trans} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 import {useFocusEffect, useNavigation} from '@react-navigation/native' 6 7 import {useEmail} from '#/lib/hooks/useEmail' 8 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 9 import {NavigationProp} from '#/lib/routes/types' 10 import {useModalControls} from '#/state/modals' 11 import {useSetMinimalShellMode} from '#/state/shell' 12 import {MyLists} from '#/view/com/lists/MyLists' 13 + import {atoms as a} from '#/alf' 14 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 15 import {useDialogControl} from '#/components/Dialog' 16 import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 17 + import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 18 import * as Layout from '#/components/Layout' 19 20 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'> 21 export function ListsScreen({}: Props) { 22 const {_} = useLingui() 23 const setMinimalShellMode = useSetMinimalShellMode() 24 const navigation = useNavigation<NavigationProp>() 25 const {openModal} = useModalControls() 26 const {needsEmailVerification} = useEmail() ··· 55 56 return ( 57 <Layout.Screen testID="listsScreen"> 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> 65 <Trans>Public, shareable lists which can drive feeds.</Trans> 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} /> 82 <VerifyEmailDialog 83 reasonText={_( 84 msg`Before creating a list, you must first verify your email.`,
+6 -20
src/view/screens/ModerationBlockedAccounts.tsx
··· 23 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 24 import {Text} from '#/view/com/util/text/Text' 25 import {ViewHeader} from '#/view/com/util/ViewHeader' 26 - import {CenteredView} from '#/view/com/util/Views' 27 import * as Layout from '#/components/Layout' 28 29 type Props = NativeStackScreenProps< ··· 97 ) 98 return ( 99 <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"> 108 <ViewHeader title={_(msg`Blocked Accounts`)} showOnDesktop /> 109 <Text 110 type="sm" ··· 112 styles.description, 113 pal.text, 114 isTabletOrDesktop && styles.descriptionDesktop, 115 ]}> 116 <Trans> 117 Blocked accounts cannot reply in your threads, mention you, or ··· 120 </Trans> 121 </Text> 122 {isEmpty ? ( 123 - <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}> 124 {isError ? ( 125 <ErrorScreen 126 title="Oops!" ··· 166 desktopFixedHeight 167 /> 168 )} 169 - </CenteredView> 170 </Layout.Screen> 171 ) 172 } 173 174 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 title: { 185 textAlign: 'center', 186 marginTop: 12,
··· 23 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 24 import {Text} from '#/view/com/util/text/Text' 25 import {ViewHeader} from '#/view/com/util/ViewHeader' 26 import * as Layout from '#/components/Layout' 27 28 type Props = NativeStackScreenProps< ··· 96 ) 97 return ( 98 <Layout.Screen testID="blockedAccountsScreen"> 99 + <Layout.Center> 100 <ViewHeader title={_(msg`Blocked Accounts`)} showOnDesktop /> 101 <Text 102 type="sm" ··· 104 styles.description, 105 pal.text, 106 isTabletOrDesktop && styles.descriptionDesktop, 107 + { 108 + marginTop: 20, 109 + }, 110 ]}> 111 <Trans> 112 Blocked accounts cannot reply in your threads, mention you, or ··· 115 </Trans> 116 </Text> 117 {isEmpty ? ( 118 + <View style={[pal.border]}> 119 {isError ? ( 120 <ErrorScreen 121 title="Oops!" ··· 161 desktopFixedHeight 162 /> 163 )} 164 + </Layout.Center> 165 </Layout.Screen> 166 ) 167 } 168 169 const styles = StyleSheet.create({ 170 title: { 171 textAlign: 'center', 172 marginTop: 12,
+25 -39
src/view/screens/ModerationModlists.tsx
··· 1 import React from 'react' 2 - import {View} from 'react-native' 3 import {AtUri} from '@atproto/api' 4 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 import {msg, Trans} from '@lingui/macro' 6 import {useLingui} from '@lingui/react' 7 import {useFocusEffect, useNavigation} from '@react-navigation/native' 8 9 import {useEmail} from '#/lib/hooks/useEmail' 10 - import {usePalette} from '#/lib/hooks/usePalette' 11 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 12 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 13 import {NavigationProp} from '#/lib/routes/types' 14 - import {s} from '#/lib/styles' 15 import {useModalControls} from '#/state/modals' 16 import {useSetMinimalShellMode} from '#/state/shell' 17 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' 21 import {useDialogControl} from '#/components/Dialog' 22 import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 23 import * as Layout from '#/components/Layout' 24 25 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'> 26 export function ModerationModlistsScreen({}: Props) { 27 const {_} = useLingui() 28 - const pal = usePalette('default') 29 const setMinimalShellMode = useSetMinimalShellMode() 30 - const {isMobile} = useWebMediaQueries() 31 const navigation = useNavigation<NavigationProp>() 32 const {openModal} = useModalControls() 33 const {needsEmailVerification} = useEmail() ··· 62 63 return ( 64 <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'}]}> 72 <Trans>Moderation Lists</Trans> 73 - </Text> 74 - <Text style={pal.textLight}> 75 <Trans> 76 Public, shareable lists of users to mute or block in bulk. 77 </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} /> 98 <VerifyEmailDialog 99 reasonText={_( 100 msg`Before creating a list, you must first verify your email.`,
··· 1 import React from 'react' 2 import {AtUri} from '@atproto/api' 3 import {msg, Trans} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 import {useFocusEffect, useNavigation} from '@react-navigation/native' 6 7 import {useEmail} from '#/lib/hooks/useEmail' 8 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 9 import {NavigationProp} from '#/lib/routes/types' 10 import {useModalControls} from '#/state/modals' 11 import {useSetMinimalShellMode} from '#/state/shell' 12 import {MyLists} from '#/view/com/lists/MyLists' 13 + import {atoms as a} from '#/alf' 14 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 15 import {useDialogControl} from '#/components/Dialog' 16 import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 17 + import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 18 import * as Layout from '#/components/Layout' 19 20 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'> 21 export function ModerationModlistsScreen({}: Props) { 22 const {_} = useLingui() 23 const setMinimalShellMode = useSetMinimalShellMode() 24 const navigation = useNavigation<NavigationProp>() 25 const {openModal} = useModalControls() 26 const {needsEmailVerification} = useEmail() ··· 55 56 return ( 57 <Layout.Screen testID="moderationModlistsScreen"> 58 + <Layout.Header.Outer> 59 + <Layout.Header.BackButton /> 60 + <Layout.Header.Content align="left"> 61 + <Layout.Header.TitleText> 62 <Trans>Moderation Lists</Trans> 63 + </Layout.Header.TitleText> 64 + <Layout.Header.SubtitleText> 65 <Trans> 66 Public, shareable lists of users to mute or block in bulk. 67 </Trans> 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} /> 84 <VerifyEmailDialog 85 reasonText={_( 86 msg`Before creating a list, you must first verify your email.`,
+7 -21
src/view/screens/ModerationMutedAccounts.tsx
··· 23 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 24 import {Text} from '#/view/com/util/text/Text' 25 import {ViewHeader} from '#/view/com/util/ViewHeader' 26 - import {CenteredView} from '#/view/com/util/Views' 27 import * as Layout from '#/components/Layout' 28 29 type Props = NativeStackScreenProps< ··· 97 ) 98 return ( 99 <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 /> 109 <Text 110 type="sm" 111 style={[ 112 styles.description, 113 pal.text, 114 isTabletOrDesktop && styles.descriptionDesktop, 115 ]}> 116 <Trans> 117 Muted accounts have their posts removed from your feed and from your ··· 119 </Trans> 120 </Text> 121 {isEmpty ? ( 122 - <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}> 123 {isError ? ( 124 <ErrorScreen 125 title="Oops!" ··· 165 desktopFixedHeight 166 /> 167 )} 168 - </CenteredView> 169 </Layout.Screen> 170 ) 171 } 172 173 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 title: { 184 textAlign: 'center', 185 marginTop: 12,
··· 23 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 24 import {Text} from '#/view/com/util/text/Text' 25 import {ViewHeader} from '#/view/com/util/ViewHeader' 26 import * as Layout from '#/components/Layout' 27 28 type Props = NativeStackScreenProps< ··· 96 ) 97 return ( 98 <Layout.Screen testID="mutedAccountsScreen"> 99 + <ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop /> 100 + <Layout.Center> 101 <Text 102 type="sm" 103 style={[ 104 styles.description, 105 pal.text, 106 isTabletOrDesktop && styles.descriptionDesktop, 107 + { 108 + marginTop: 20, 109 + }, 110 ]}> 111 <Trans> 112 Muted accounts have their posts removed from your feed and from your ··· 114 </Trans> 115 </Text> 116 {isEmpty ? ( 117 + <View style={[pal.border]}> 118 {isError ? ( 119 <ErrorScreen 120 title="Oops!" ··· 160 desktopFixedHeight 161 /> 162 )} 163 + </Layout.Center> 164 </Layout.Screen> 165 ) 166 } 167 168 const styles = StyleSheet.create({ 169 title: { 170 textAlign: 'center', 171 marginTop: 12,
+65 -113
src/view/screens/Notifications.tsx
··· 1 - import React, {useCallback} from 'react' 2 import {View} from 'react-native' 3 import {msg, Trans} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' ··· 6 import {useQueryClient} from '@tanstack/react-query' 7 8 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 9 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 10 import {ComposeIcon2} from '#/lib/icons' 11 import { 12 NativeStackScreenProps, ··· 14 } from '#/lib/routes/types' 15 import {s} from '#/lib/styles' 16 import {logger} from '#/logger' 17 - import {isNative} from '#/platform/detection' 18 import {emitSoftReset, listenSoftReset} from '#/state/events' 19 import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed' 20 import { ··· 29 import {ListMethods} from '#/view/com/util/List' 30 import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' 31 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' 36 import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2' 37 import * as Layout from '#/components/Layout' 38 import {Link} from '#/components/Link' 39 import {Loader} from '#/components/Loader' 40 - import {Text} from '#/components/Typography' 41 42 type Props = NativeStackScreenProps< 43 NotificationsTabNavigatorParams, 44 'Notifications' 45 > 46 export function NotificationsScreen({route: {params}}: Props) { 47 const {_} = useLingui() 48 const setMinimalShellMode = useSetMinimalShellMode() 49 const [isScrolledDown, setIsScrolledDown] = React.useState(false) 50 const [isLoadingLatest, setIsLoadingLatest] = React.useState(false) 51 const scrollElRef = React.useRef<ListMethods>(null) 52 - const t = useTheme() 53 - const {isDesktop} = useWebMediaQueries() 54 const queryClient = useQueryClient() 55 const unreadNotifs = useUnreadNotifications() 56 const unreadApi = useUnreadNotificationsApi() ··· 110 return listenSoftReset(onPressLoadLatest) 111 }, [onPressLoadLatest, isScreenFocused]) 112 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 - ]}> 142 <Button 143 label={_(msg`Notifications`)} 144 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 - ]}> 153 <Trans>Notifications</Trans> 154 - {hasNew && ( 155 <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 - }} 164 /> 165 )} 166 - </Text> 167 )} 168 </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]) 178 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} 203 /> 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="" 226 /> 227 - </CenteredView> 228 </Layout.Screen> 229 ) 230 }
··· 1 + import React from 'react' 2 import {View} from 'react-native' 3 import {msg, Trans} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' ··· 6 import {useQueryClient} from '@tanstack/react-query' 7 8 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 9 import {ComposeIcon2} from '#/lib/icons' 10 import { 11 NativeStackScreenProps, ··· 13 } from '#/lib/routes/types' 14 import {s} from '#/lib/styles' 15 import {logger} from '#/logger' 16 + import {isNative, isWeb} from '#/platform/detection' 17 import {emitSoftReset, listenSoftReset} from '#/state/events' 18 import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed' 19 import { ··· 28 import {ListMethods} from '#/view/com/util/List' 29 import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' 30 import {MainScrollProvider} from '#/view/com/util/MainScrollProvider' 31 + import {atoms as a, useBreakpoints, useTheme} from '#/alf' 32 + import {Button, ButtonIcon} from '#/components/Button' 33 import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2' 34 import * as Layout from '#/components/Layout' 35 import {Link} from '#/components/Link' 36 import {Loader} from '#/components/Loader' 37 38 type Props = NativeStackScreenProps< 39 NotificationsTabNavigatorParams, 40 'Notifications' 41 > 42 export function NotificationsScreen({route: {params}}: Props) { 43 + const t = useTheme() 44 + const {gtTablet} = useBreakpoints() 45 const {_} = useLingui() 46 const setMinimalShellMode = useSetMinimalShellMode() 47 const [isScrolledDown, setIsScrolledDown] = React.useState(false) 48 const [isLoadingLatest, setIsLoadingLatest] = React.useState(false) 49 const scrollElRef = React.useRef<ListMethods>(null) 50 const queryClient = useQueryClient() 51 const unreadNotifs = useUnreadNotifications() 52 const unreadApi = useUnreadNotificationsApi() ··· 106 return listenSoftReset(onPressLoadLatest) 107 }, [onPressLoadLatest, isScreenFocused]) 108 109 + return ( 110 + <Layout.Screen testID="notificationsScreen"> 111 + <Layout.Header.Outer> 112 + <Layout.Header.MenuButton /> 113 + <Layout.Header.Content> 114 <Button 115 label={_(msg`Notifications`)} 116 accessibilityHint={_(msg`Refresh notifications`)} 117 + onPress={emitSoftReset} 118 + style={[a.justify_start]}> 119 + {({hovered}) => ( 120 + <Layout.Header.TitleText 121 + style={[a.w_full, hovered && a.underline]}> 122 <Trans>Notifications</Trans> 123 + {isWeb && gtTablet && hasNew && ( 124 <View 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 + ]} 135 /> 136 )} 137 + </Layout.Header.TitleText> 138 )} 139 </Button> 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> 157 158 + <MainScrollProvider> 159 + <Feed 160 + onScrolledDownChange={setIsScrolledDown} 161 + scrollElRef={scrollElRef} 162 + overridePriorityNotifications={params?.show === 'all'} 163 /> 164 + </MainScrollProvider> 165 + {(isScrolledDown || hasNew) && ( 166 + <LoadLatestBtn 167 + onPress={onPressLoadLatest} 168 + label={_(msg`Load new notifications`)} 169 + showIndicator={hasNew} 170 /> 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 + /> 180 </Layout.Screen> 181 ) 182 }
+1 -5
src/view/screens/PostThread.tsx
··· 1 import React from 'react' 2 - import {View} from 'react-native' 3 import {useFocusEffect} from '@react-navigation/native' 4 5 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 6 import {makeRecordUri} from '#/lib/strings/url-helpers' 7 import {useSetMinimalShellMode} from '#/state/shell' 8 import {PostThread as PostThreadComponent} from '#/view/com/post-thread/PostThread' 9 - import {atoms as a} from '#/alf' 10 import * as Layout from '#/components/Layout' 11 12 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'> ··· 24 25 return ( 26 <Layout.Screen testID="postThreadScreen"> 27 - <View style={a.flex_1}> 28 - <PostThreadComponent uri={uri} /> 29 - </View> 30 </Layout.Screen> 31 ) 32 }
··· 1 import React from 'react' 2 import {useFocusEffect} from '@react-navigation/native' 3 4 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 5 import {makeRecordUri} from '#/lib/strings/url-helpers' 6 import {useSetMinimalShellMode} from '#/state/shell' 7 import {PostThread as PostThreadComponent} from '#/view/com/post-thread/PostThread' 8 import * as Layout from '#/components/Layout' 9 10 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'> ··· 22 23 return ( 24 <Layout.Screen testID="postThreadScreen"> 25 + <PostThreadComponent uri={uri} /> 26 </Layout.Screen> 27 ) 28 }
+2 -4
src/view/screens/Profile.tsx
··· 40 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 41 import {FAB} from '#/view/com/util/fab/FAB' 42 import {ListRef} from '#/view/com/util/List' 43 - import {CenteredView} from '#/view/com/util/Views' 44 import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header' 45 import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed' 46 import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels' 47 - import {web} from '#/alf' 48 import * as Layout from '#/components/Layout' 49 import {ScreenHider} from '#/components/moderation/ScreenHider' 50 import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks' ··· 116 // Most pushes will happen here, since we will have only placeholder data 117 if (isLoadingDid || isLoadingProfile || starterPacksQuery.isLoading) { 118 return ( 119 - <CenteredView sideBorders style={web({height: '100vh'})}> 120 <ProfileHeaderLoading /> 121 - </CenteredView> 122 ) 123 } 124 if (resolveError || profileError) {
··· 40 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 41 import {FAB} from '#/view/com/util/fab/FAB' 42 import {ListRef} from '#/view/com/util/List' 43 import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header' 44 import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed' 45 import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels' 46 import * as Layout from '#/components/Layout' 47 import {ScreenHider} from '#/components/moderation/ScreenHider' 48 import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks' ··· 114 // Most pushes will happen here, since we will have only placeholder data 115 if (isLoadingDid || isLoadingProfile || starterPacksQuery.isLoading) { 116 return ( 117 + <Layout.Content> 118 <ProfileHeaderLoading /> 119 + </Layout.Content> 120 ) 121 } 122 if (resolveError || profileError) {
+4 -5
src/view/screens/ProfileFeed.tsx
··· 49 import {LoadingScreen} from '#/view/com/util/LoadingScreen' 50 import {Text} from '#/view/com/util/text/Text' 51 import * as Toast from '#/view/com/util/Toast' 52 - import {CenteredView} from '#/view/com/util/Views' 53 import {atoms as a, useTheme} from '#/alf' 54 import {Button as NewButton, ButtonText} from '#/components/Button' 55 import {useRichText} from '#/components/hooks/useRichText' ··· 98 if (error) { 99 return ( 100 <Layout.Screen testID="profileFeedScreenError"> 101 - <CenteredView> 102 <View style={[pal.view, pal.border, styles.notFoundContainer]}> 103 <Text type="title-lg" style={[pal.text, s.mb10]}> 104 <Trans>Could not load feed</Trans> ··· 120 </Button> 121 </View> 122 </View> 123 - </CenteredView> 124 </Layout.Screen> 125 ) 126 } ··· 394 ]) 395 396 return ( 397 - <View style={s.hContentRegion}> 398 <ReportDialog 399 control={reportDialogControl} 400 params={{ ··· 434 accessibilityHint="" 435 /> 436 )} 437 - </View> 438 ) 439 } 440
··· 49 import {LoadingScreen} from '#/view/com/util/LoadingScreen' 50 import {Text} from '#/view/com/util/text/Text' 51 import * as Toast from '#/view/com/util/Toast' 52 import {atoms as a, useTheme} from '#/alf' 53 import {Button as NewButton, ButtonText} from '#/components/Button' 54 import {useRichText} from '#/components/hooks/useRichText' ··· 97 if (error) { 98 return ( 99 <Layout.Screen testID="profileFeedScreenError"> 100 + <Layout.Content> 101 <View style={[pal.view, pal.border, styles.notFoundContainer]}> 102 <Text type="title-lg" style={[pal.text, s.mb10]}> 103 <Trans>Could not load feed</Trans> ··· 119 </Button> 120 </View> 121 </View> 122 + </Layout.Content> 123 </Layout.Screen> 124 ) 125 } ··· 393 ]) 394 395 return ( 396 + <> 397 <ReportDialog 398 control={reportDialogControl} 399 params={{ ··· 433 accessibilityHint="" 434 /> 435 )} 436 + </> 437 ) 438 } 439
-2
src/view/screens/ProfileFollowers.tsx
··· 10 import {ViewHeader} from '#/view/com/util/ViewHeader' 11 import {CenteredView} from '#/view/com/util/Views' 12 import * as Layout from '#/components/Layout' 13 - import {ListHeaderDesktop} from '#/components/Lists' 14 15 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'> 16 export const ProfileFollowersScreen = ({route}: Props) => { ··· 27 return ( 28 <Layout.Screen testID="profileFollowersScreen"> 29 <CenteredView sideBorders={true}> 30 - <ListHeaderDesktop title={_(msg`Followers`)} /> 31 <ViewHeader title={_(msg`Followers`)} showBorder={!isWeb} /> 32 <ProfileFollowersComponent name={name} /> 33 </CenteredView>
··· 10 import {ViewHeader} from '#/view/com/util/ViewHeader' 11 import {CenteredView} from '#/view/com/util/Views' 12 import * as Layout from '#/components/Layout' 13 14 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'> 15 export const ProfileFollowersScreen = ({route}: Props) => { ··· 26 return ( 27 <Layout.Screen testID="profileFollowersScreen"> 28 <CenteredView sideBorders={true}> 29 <ViewHeader title={_(msg`Followers`)} showBorder={!isWeb} /> 30 <ProfileFollowersComponent name={name} /> 31 </CenteredView>
-2
src/view/screens/ProfileFollows.tsx
··· 10 import {ViewHeader} from '#/view/com/util/ViewHeader' 11 import {CenteredView} from '#/view/com/util/Views' 12 import * as Layout from '#/components/Layout' 13 - import {ListHeaderDesktop} from '#/components/Lists' 14 15 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'> 16 export const ProfileFollowsScreen = ({route}: Props) => { ··· 27 return ( 28 <Layout.Screen testID="profileFollowsScreen"> 29 <CenteredView sideBorders={true}> 30 - <ListHeaderDesktop title={_(msg`Following`)} /> 31 <ViewHeader title={_(msg`Following`)} showBorder={!isWeb} /> 32 <ProfileFollowsComponent name={name} /> 33 </CenteredView>
··· 10 import {ViewHeader} from '#/view/com/util/ViewHeader' 11 import {CenteredView} from '#/view/com/util/Views' 12 import * as Layout from '#/components/Layout' 13 14 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'> 15 export const ProfileFollowsScreen = ({route}: Props) => { ··· 26 return ( 27 <Layout.Screen testID="profileFollowsScreen"> 28 <CenteredView sideBorders={true}> 29 <ViewHeader title={_(msg`Following`)} showBorder={!isWeb} /> 30 <ProfileFollowsComponent name={name} /> 31 </CenteredView>
+4 -6
src/view/screens/ProfileList.tsx
··· 69 import {LoadingScreen} from '#/view/com/util/LoadingScreen' 70 import {Text} from '#/view/com/util/text/Text' 71 import * as Toast from '#/view/com/util/Toast' 72 - import {CenteredView} from '#/view/com/util/Views' 73 import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen' 74 import {atoms as a, useTheme} from '#/alf' 75 import {useDialogControl} from '#/components/Dialog' ··· 107 108 if (resolveError) { 109 return ( 110 - <CenteredView> 111 <ErrorScreen 112 error={_( 113 msg`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`, 114 )} 115 /> 116 - </CenteredView> 117 ) 118 } 119 if (listError) { 120 return ( 121 - <CenteredView> 122 <ErrorScreen error={cleanError(listError)} /> 123 - </CenteredView> 124 ) 125 } 126 ··· 1010 pal.view, 1011 pal.border, 1012 { 1013 - marginTop: 10, 1014 paddingHorizontal: 18, 1015 paddingVertical: 14, 1016 borderTopWidth: StyleSheet.hairlineWidth,
··· 69 import {LoadingScreen} from '#/view/com/util/LoadingScreen' 70 import {Text} from '#/view/com/util/text/Text' 71 import * as Toast from '#/view/com/util/Toast' 72 import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen' 73 import {atoms as a, useTheme} from '#/alf' 74 import {useDialogControl} from '#/components/Dialog' ··· 106 107 if (resolveError) { 108 return ( 109 + <Layout.Content> 110 <ErrorScreen 111 error={_( 112 msg`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`, 113 )} 114 /> 115 + </Layout.Content> 116 ) 117 } 118 if (listError) { 119 return ( 120 + <Layout.Content> 121 <ErrorScreen error={cleanError(listError)} /> 122 + </Layout.Content> 123 ) 124 } 125 ··· 1009 pal.view, 1010 pal.border, 1011 { 1012 paddingHorizontal: 18, 1013 paddingVertical: 14, 1014 borderTopWidth: StyleSheet.hairlineWidth,
+114 -124
src/view/screens/SavedFeeds.tsx
··· 25 import {TextLink} from '#/view/com/util/Link' 26 import {Text} from '#/view/com/util/text/Text' 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 import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed' 31 import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType' 32 import {atoms as a, useTheme} from '#/alf' 33 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 34 import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' 35 import * as Layout from '#/components/Layout' 36 import {Loader} from '#/components/Loader' 37 ··· 51 }) { 52 const pal = usePalette('default') 53 const {_} = useLingui() 54 - const {isMobile, isTabletOrDesktop, isDesktop} = useWebMediaQueries() 55 const setMinimalShellMode = useSetMinimalShellMode() 56 const {mutateAsync: overwriteSavedFeeds, isPending: isOverwritePending} = 57 useOverwriteSavedFeedsMutation() ··· 88 } 89 }, [_, overwriteSavedFeeds, currentFeeds, navigation]) 90 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 return ( 111 <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 - )} 127 128 - <View style={[pal.text, pal.border, styles.title]}> 129 - <Text type="title" style={pal.text}> 130 - <Trans>Pinned Feeds</Trans> 131 - </Text> 132 </View> 133 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 - )} 162 163 - {noFollowingFeed && ( 164 - <View style={[pal.border, a.border_b]}> 165 - <NoFollowingFeed /> 166 </View> 167 - )} 168 169 - <View style={[pal.text, pal.border, styles.title]}> 170 - <Text type="title" style={pal.text}> 171 - <Trans>Saved Feeds</Trans> 172 - </Text> 173 </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 - ) 199 ) : ( 200 - <ActivityIndicator style={{marginTop: 20}} /> 201 - )} 202 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> 221 </Layout.Screen> 222 ) 223 } ··· 456 }, 457 footerText: { 458 paddingHorizontal: 26, 459 - paddingTop: 22, 460 - paddingBottom: 100, 461 }, 462 })
··· 25 import {TextLink} from '#/view/com/util/Link' 26 import {Text} from '#/view/com/util/text/Text' 27 import * as Toast from '#/view/com/util/Toast' 28 import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed' 29 import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType' 30 import {atoms as a, useTheme} from '#/alf' 31 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 32 import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' 33 + import {FloppyDisk_Stroke2_Corner0_Rounded as SaveIcon} from '#/components/icons/FloppyDisk' 34 import * as Layout from '#/components/Layout' 35 import {Loader} from '#/components/Loader' 36 ··· 50 }) { 51 const pal = usePalette('default') 52 const {_} = useLingui() 53 + const {isMobile, isDesktop} = useWebMediaQueries() 54 const setMinimalShellMode = useSetMinimalShellMode() 55 const {mutateAsync: overwriteSavedFeeds, isPending: isOverwritePending} = 56 useOverwriteSavedFeedsMutation() ··· 87 } 88 }, [_, overwriteSavedFeeds, currentFeeds, navigation]) 89 90 return ( 91 <Layout.Screen> 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> 113 114 + <Layout.Content> 115 + {noSavedFeedsOfAnyType && ( 116 + <View style={[pal.border, a.border_b]}> 117 + <NoSavedFeedsOfAnyType /> 118 </View> 119 + )} 120 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> 126 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> 139 </View> 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 + )} 155 156 + {noFollowingFeed && ( 157 + <View style={[pal.border, a.border_b]}> 158 + <NoFollowingFeed /> 159 </View> 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> 180 ) : ( 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 + )} 195 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> 212 </Layout.Screen> 213 ) 214 } ··· 447 }, 448 footerText: { 449 paddingHorizontal: 26, 450 + paddingVertical: 22, 451 }, 452 })
+105 -131
src/view/screens/Search/Search.tsx
··· 55 import {Link} from '#/view/com/util/Link' 56 import {List} from '#/view/com/util/List' 57 import {Text} from '#/view/com/util/text/Text' 58 - import {CenteredView, ScrollView} from '#/view/com/util/Views' 59 import {Explore} from '#/view/screens/Search/Explore' 60 import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search' 61 import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils' ··· 68 import * as Layout from '#/components/Layout' 69 70 function Loader() { 71 - const pal = usePalette('default') 72 - const {isMobile} = useWebMediaQueries() 73 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> 86 ) 87 } 88 89 function EmptyState({message, error}: {message: string; error?: string}) { 90 const pal = usePalette('default') 91 - const {isMobile} = useWebMediaQueries() 92 93 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> 106 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 - /> 120 121 - <Text style={[pal.textLight]}> 122 - <Trans>Error:</Trans> {error} 123 - </Text> 124 - </> 125 - )} 126 </View> 127 - </CenteredView> 128 ) 129 } 130 ··· 224 if (item.type === 'post') { 225 return <Post post={item.post} /> 226 } else { 227 - return <Loader /> 228 } 229 }} 230 keyExtractor={item => item.key} ··· 550 <Pager 551 onPageSelected={onPageSelected} 552 renderTabBar={props => ( 553 - <CenteredView 554 - sideBorders 555 style={[ 556 - pal.border, 557 - pal.view, 558 - web({ 559 - position: isWeb ? 'sticky' : '', 560 - zIndex: 1, 561 - }), 562 {top: isWeb ? headerHeight : undefined}, 563 ]}> 564 <TabBar items={sections.map(section => section.title)} {...props} /> 565 - </CenteredView> 566 )} 567 initialPage={0}> 568 {sections.map((section, i) => ( ··· 572 ) : hasSession ? ( 573 <Explore /> 574 ) : ( 575 - <CenteredView sideBorders style={pal.border}> 576 <View 577 // @ts-ignore web only -esb 578 style={{ ··· 614 </Text> 615 </View> 616 </View> 617 - </CenteredView> 618 ) 619 } 620 SearchScreenInner = React.memo(SearchScreenInner) ··· 650 * Arbitrary sizing, so guess and check, used for sticky header alignment and 651 * sizing. 652 */ 653 - const headerHeight = 64 + (showFilters ? 40 : 0) 654 655 useFocusEffect( 656 useNonReactiveCallback(() => { ··· 861 862 return ( 863 <Layout.Screen testID="searchScreen"> 864 - <CenteredView 865 style={[ 866 - a.p_md, 867 - a.pb_sm, 868 - a.gap_sm, 869 - t.atoms.bg, 870 web({ 871 height: headerHeight, 872 position: 'sticky', 873 top: 0, 874 zIndex: 1, 875 }), 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 - /> 927 </View> 928 </View> 929 - )} 930 - </CenteredView> 931 932 <View 933 style={{ ··· 992 !moderationOpts ? ( 993 <Loader /> 994 ) : ( 995 - <ScrollView 996 - style={{height: '100%'}} 997 - // @ts-ignore web only -prf 998 - dataSet={{stableGutters: '1'}} 999 keyboardShouldPersistTaps="handled" 1000 keyboardDismissMode="on-drag"> 1001 <SearchLinkCard ··· 1020 /> 1021 ))} 1022 <View style={{height: 200}} /> 1023 - </ScrollView> 1024 )} 1025 </> 1026 ) ··· 1042 onRemoveItemClick: (item: string) => void 1043 onRemoveProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void 1044 }) { 1045 - const {isTabletOrDesktop, isMobile} = useWebMediaQueries() 1046 const pal = usePalette('default') 1047 const {_} = useLingui() 1048 1049 return ( 1050 - <CenteredView 1051 - sideBorders={isTabletOrDesktop} 1052 - // @ts-ignore web only -prf 1053 - style={{ 1054 - height: isWeb ? '100vh' : undefined, 1055 - }}> 1056 <View style={styles.searchHistoryContainer}> 1057 {(searchHistory.length > 0 || selectedProfiles.length > 0) && ( 1058 <Text style={[pal.text, styles.searchHistoryTitle]}> ··· 1152 </View> 1153 )} 1154 </View> 1155 - </CenteredView> 1156 ) 1157 } 1158
··· 55 import {Link} from '#/view/com/util/Link' 56 import {List} from '#/view/com/util/List' 57 import {Text} from '#/view/com/util/text/Text' 58 import {Explore} from '#/view/screens/Search/Explore' 59 import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search' 60 import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils' ··· 67 import * as Layout from '#/components/Layout' 68 69 function Loader() { 70 return ( 71 + <Layout.Content> 72 + <View style={[a.py_xl]}> 73 + <ActivityIndicator /> 74 + </View> 75 + </Layout.Content> 76 ) 77 } 78 79 function EmptyState({message, error}: {message: string; error?: string}) { 80 const pal = usePalette('default') 81 82 return ( 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> 87 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 + /> 101 102 + <Text style={[pal.textLight]}> 103 + <Trans>Error:</Trans> {error} 104 + </Text> 105 + </> 106 + )} 107 + </View> 108 </View> 109 + </Layout.Content> 110 ) 111 } 112 ··· 206 if (item.type === 'post') { 207 return <Post post={item.post} /> 208 } else { 209 + return null 210 } 211 }} 212 keyExtractor={item => item.key} ··· 532 <Pager 533 onPageSelected={onPageSelected} 534 renderTabBar={props => ( 535 + <Layout.Center 536 style={[ 537 + web([a.sticky, a.z_10]), 538 {top: isWeb ? headerHeight : undefined}, 539 ]}> 540 <TabBar items={sections.map(section => section.title)} {...props} /> 541 + </Layout.Center> 542 )} 543 initialPage={0}> 544 {sections.map((section, i) => ( ··· 548 ) : hasSession ? ( 549 <Explore /> 550 ) : ( 551 + <Layout.Center> 552 <View 553 // @ts-ignore web only -esb 554 style={{ ··· 590 </Text> 591 </View> 592 </View> 593 + </Layout.Center> 594 ) 595 } 596 SearchScreenInner = React.memo(SearchScreenInner) ··· 626 * Arbitrary sizing, so guess and check, used for sticky header alignment and 627 * sizing. 628 */ 629 + const headerHeight = 60 + (showFilters ? 40 : 0) 630 631 useFocusEffect( 632 useNonReactiveCallback(() => { ··· 837 838 return ( 839 <Layout.Screen testID="searchScreen"> 840 + <View 841 style={[ 842 web({ 843 height: headerHeight, 844 position: 'sticky', 845 top: 0, 846 zIndex: 1, 847 }), 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 + )} 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 + )} 910 </View> 911 + </Layout.Center> 912 + </View> 913 914 <View 915 style={{ ··· 974 !moderationOpts ? ( 975 <Loader /> 976 ) : ( 977 + <Layout.Content 978 keyboardShouldPersistTaps="handled" 979 keyboardDismissMode="on-drag"> 980 <SearchLinkCard ··· 999 /> 1000 ))} 1001 <View style={{height: 200}} /> 1002 + </Layout.Content> 1003 )} 1004 </> 1005 ) ··· 1021 onRemoveItemClick: (item: string) => void 1022 onRemoveProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void 1023 }) { 1024 + const {isMobile} = useWebMediaQueries() 1025 const pal = usePalette('default') 1026 const {_} = useLingui() 1027 1028 return ( 1029 + <Layout.Content> 1030 <View style={styles.searchHistoryContainer}> 1031 {(searchHistory.length > 0 || selectedProfiles.length > 0) && ( 1032 <Text style={[pal.text, styles.searchHistoryTitle]}> ··· 1126 </View> 1127 )} 1128 </View> 1129 + </Layout.Content> 1130 ) 1131 } 1132
+7 -4
src/view/shell/Composer.web.tsx
··· 3 import {DismissableLayer} from '@radix-ui/react-dismissable-layer' 4 import {useFocusGuards} from '@radix-ui/react-focus-guards' 5 import {FocusScope} from '@radix-ui/react-focus-scope' 6 7 - import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 8 import {useModals} from '#/state/modals' 9 import {ComposerOpts, useComposerState} from '#/state/shell/composer' 10 import { ··· 20 const state = useComposerState() 21 const isActive = !!state 22 23 - useWebBodyScrollLock(isActive) 24 - 25 // rendering 26 // = 27 ··· 29 return <View /> 30 } 31 32 - return <Inner state={state} /> 33 } 34 35 function Inner({state}: {state: ComposerOpts}) {
··· 3 import {DismissableLayer} from '@radix-ui/react-dismissable-layer' 4 import {useFocusGuards} from '@radix-ui/react-focus-guards' 5 import {FocusScope} from '@radix-ui/react-focus-scope' 6 + import {RemoveScrollBar} from 'react-remove-scroll-bar' 7 8 import {useModals} from '#/state/modals' 9 import {ComposerOpts, useComposerState} from '#/state/shell/composer' 10 import { ··· 20 const state = useComposerState() 21 const isActive = !!state 22 23 // rendering 24 // = 25 ··· 27 return <View /> 28 } 29 30 + return ( 31 + <> 32 + <RemoveScrollBar /> 33 + <Inner state={state} /> 34 + </> 35 + ) 36 } 37 38 function Inner({state}: {state: ComposerOpts}) {
+53 -78
src/view/shell/desktop/LeftNav.tsx
··· 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' 7 import {msg, Trans} from '@lingui/macro' 8 import {useLingui} from '@lingui/react' 9 import { ··· 14 15 import {usePalette} from '#/lib/hooks/usePalette' 16 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 17 - import {getCurrentRoute, isStateAtTabRoot, isTab} from '#/lib/routes/helpers' 18 import {makeProfileLink} from '#/lib/routes/links' 19 - import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' 20 import {isInvalidHandle} from '#/lib/strings/handles' 21 import {emitSoftReset} from '#/state/events' 22 import {useFetchHandle} from '#/state/queries/handle' ··· 101 ) 102 } 103 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 interface NavItemProps { 146 count?: string 147 href: string ··· 220 ]}> 221 {isCurrent ? iconFilled : icon} 222 {typeof count === 'string' && count ? ( 223 - <Text 224 - accessibilityLabel={_(msg`${count} unread items`)} 225 - accessibilityHint="" 226 - accessible={true} 227 style={[ 228 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 && [ 244 { 245 - top: '10%', 246 - left: count.length === 1 ? '50%' : '40%', 247 }, 248 - ], 249 - ]}> 250 - {count} 251 - </Text> 252 ) : null} 253 </View> 254 {gtTablet && ( ··· 366 <View 367 role="navigation" 368 style={[ 369 styles.leftNav, 370 isTablet && styles.leftNavTablet, 371 - pal.view, 372 pal.border, 373 ]}> 374 {hasSession ? ( ··· 381 382 {hasSession && ( 383 <> 384 - <BackBtn /> 385 - 386 <NavItem 387 href="/" 388 icon={ ··· 525 position: 'fixed', 526 top: 10, 527 // @ts-ignore web only 528 - left: 'calc(50vw - 300px - 220px - 20px)', 529 - width: 220, 530 // @ts-ignore web only 531 maxHeight: 'calc(100vh - 10px)', 532 overflowY: 'auto', ··· 538 borderRightWidth: 1, 539 height: '100%', 540 width: 76, 541 alignItems: 'center', 542 }, 543 544 profileCard: {
··· 1 import React from 'react' 2 + import {StyleSheet, View} from 'react-native' 3 + import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 import { ··· 11 12 import {usePalette} from '#/lib/hooks/usePalette' 13 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 14 + import {getCurrentRoute, isTab} from '#/lib/routes/helpers' 15 import {makeProfileLink} from '#/lib/routes/links' 16 + import {CommonNavigatorParams} from '#/lib/routes/types' 17 import {isInvalidHandle} from '#/lib/strings/handles' 18 import {emitSoftReset} from '#/state/events' 19 import {useFetchHandle} from '#/state/queries/handle' ··· 98 ) 99 } 100 101 interface NavItemProps { 102 count?: string 103 href: string ··· 176 ]}> 177 {isCurrent ? iconFilled : icon} 178 {typeof count === 'string' && count ? ( 179 + <View 180 style={[ 181 a.absolute, 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, 197 { 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, 206 }, 207 + isTablet && [ 208 + { 209 + top: '10%', 210 + left: count.length === 1 ? 20 : 16, 211 + }, 212 + ], 213 + ]}> 214 + {count} 215 + </Text> 216 + </View> 217 ) : null} 218 </View> 219 {gtTablet && ( ··· 331 <View 332 role="navigation" 333 style={[ 334 + a.px_xl, 335 styles.leftNav, 336 isTablet && styles.leftNavTablet, 337 pal.border, 338 ]}> 339 {hasSession ? ( ··· 346 347 {hasSession && ( 348 <> 349 <NavItem 350 href="/" 351 icon={ ··· 488 position: 'fixed', 489 top: 10, 490 // @ts-ignore web only 491 + left: '50%', 492 + transform: [ 493 + { 494 + translateX: -300, 495 + }, 496 + { 497 + translateX: '-100%', 498 + }, 499 + ...a.scrollbar_offset.transform, 500 + ], 501 + width: 240, 502 // @ts-ignore web only 503 maxHeight: 'calc(100vh - 10px)', 504 overflowY: 'auto', ··· 510 borderRightWidth: 1, 511 height: '100%', 512 width: 76, 513 + paddingLeft: 0, 514 + paddingRight: 0, 515 alignItems: 'center', 516 + transform: [], 517 }, 518 519 profileCard: {
+8 -3
src/view/shell/desktop/RightNav.tsx
··· 28 } 29 30 return ( 31 - <View style={[styles.rightNav, pal.view]}> 32 <View style={{paddingVertical: 20}}> 33 {routeName === 'Search' ? ( 34 <View style={{marginBottom: 18}}> ··· 122 // @ts-ignore web only 123 position: 'fixed', 124 // @ts-ignore web only 125 - left: 'calc(50vw + 300px + 20px)', 126 - width: 300, 127 maxHeight: '100%', 128 overflowY: 'auto', 129 },
··· 28 } 29 30 return ( 31 + <View style={[a.px_xl, styles.rightNav]}> 32 <View style={{paddingVertical: 20}}> 33 {routeName === 'Search' ? ( 34 <View style={{marginBottom: 18}}> ··· 122 // @ts-ignore web only 123 position: 'fixed', 124 // @ts-ignore web only 125 + left: '50%', 126 + transform: [ 127 + { 128 + translateX: 300, 129 + }, 130 + ...a.scrollbar_offset.transform, 131 + ], 132 maxHeight: '100%', 133 overflowY: 'auto', 134 },
+28 -26
src/view/shell/index.web.tsx
··· 3 import {msg} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 import {useNavigation} from '@react-navigation/native' 6 7 import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' 8 import {useIntentHandler} from '#/lib/hooks/useIntentHandler' 9 - import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 10 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 11 import {NavigationProp} from '#/lib/routes/types' 12 import {colors} from '#/lib/styles' ··· 34 const {_} = useLingui() 35 const showDrawer = !isDesktop && isDrawerOpen 36 37 - useWebBodyScrollLock(showDrawer) 38 useComposerKeyboardShortcut() 39 useIntentHandler() 40 ··· 58 <PortalOutlet /> 59 60 {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 /> 83 </View> 84 - </View> 85 - </TouchableWithoutFeedback> 86 )} 87 </> 88 )
··· 3 import {msg} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 import {useNavigation} from '@react-navigation/native' 6 + import {RemoveScrollBar} from 'react-remove-scroll-bar' 7 8 import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' 9 import {useIntentHandler} from '#/lib/hooks/useIntentHandler' 10 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 11 import {NavigationProp} from '#/lib/routes/types' 12 import {colors} from '#/lib/styles' ··· 34 const {_} = useLingui() 35 const showDrawer = !isDesktop && isDrawerOpen 36 37 useComposerKeyboardShortcut() 38 useIntentHandler() 39 ··· 57 <PortalOutlet /> 58 59 {showDrawer && ( 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> 85 </View> 86 + </TouchableWithoutFeedback> 87 + </> 88 )} 89 </> 90 )
+7 -2
web/index.html
··· 45 } 46 html { 47 background-color: white; 48 - scrollbar-gutter: stable both-edges; 49 } 50 @media (prefers-color-scheme: dark) { 51 html { ··· 81 top: 50%; 82 transform: translateX(-50%) translateY(-50%) translateY(-50px); 83 } 84 - /* We need this style to prevent web dropdowns from shifting the display when opening */ 85 body { 86 width: 100%; 87 } 88 </style> 89 </head>
··· 45 } 46 html { 47 background-color: white; 48 } 49 @media (prefers-color-scheme: dark) { 50 html { ··· 80 top: 50%; 81 transform: translateX(-50%) translateY(-50%) translateY(-50px); 82 } 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 + */ 89 body { 90 width: 100%; 91 + overflow-y: scroll; 92 } 93 </style> 94 </head>
+3 -28
yarn.lock
··· 17616 resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" 17617 integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== 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: 17629 version "4.2.3" 17630 resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" 17631 integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== ··· 17725 dependencies: 17726 safe-buffer "~5.1.0" 17727 17728 - "strip-ansi-cjs@npm:strip-ansi@^6.0.1": 17729 version "6.0.1" 17730 resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" 17731 integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== ··· 17738 integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== 17739 dependencies: 17740 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 17749 strip-ansi@^7.0.1: 17750 version "7.1.0" ··· 19068 resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" 19069 integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== 19070 19071 - "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": 19072 version "7.0.0" 19073 resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" 19074 integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== ··· 19081 version "6.2.0" 19082 resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" 19083 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 dependencies: 19094 ansi-styles "^4.0.0" 19095 string-width "^4.1.0"
··· 17616 resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" 17617 integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== 17618 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: 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== ··· 17716 dependencies: 17717 safe-buffer "~5.1.0" 17718 17719 + "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: 17720 version "6.0.1" 17721 resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" 17722 integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== ··· 17729 integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== 17730 dependencies: 17731 ansi-regex "^4.1.0" 17732 17733 strip-ansi@^7.0.1: 17734 version "7.1.0" ··· 19052 resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" 19053 integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== 19054 19055 + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: 19056 version "7.0.0" 19057 resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" 19058 integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== ··· 19065 version "6.2.0" 19066 resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" 19067 integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== 19068 dependencies: 19069 ansi-styles "^4.0.0" 19070 string-width "^4.1.0"