Merge remote-tracking branch 'origin/main' into eric/nova

* origin/main: (22 commits)
  basic public RSS feed for profiles (#2229)
  use `s.likeColor` everywhere (#2234)
  Add credits to localization doc (#2233)
  Fix `Logotype` fill for dark mode (#2230)
  Hide label appeal on self-labeled posts (#2232)
  Fix & Add: Japanese Translation (3) (#2226)
  tweak social card meta yet again (#2228)
  bskyweb: update golang indigo dep (from May!), and some small devex tweaks (#2227)
  Fix: Some display issues (#2219)
  Update splash screen tagline, update translations, bump ios build number
  Bump ios build number
  Tweaks (#2225)
  1.60
  Traffic reduction and tuned caching strats (#2215)
  Super secret changes don't look (#2218)
  Fix android icon dims (#2213)
  Log post creation failures (#2205)
  Some brand, some pwi (#2212)
  🤫  (#2211)
  PWI behavior updates (#2207)
  ...
This commit is contained in:
Eric Bailey
2023-12-18 16:04:43 -06:00
92 changed files with 2590 additions and 1732 deletions
+165
View File
@@ -0,0 +1,165 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {ScrollView} from '../util/Views'
import {Text} from '../util/text/Text'
import {usePalette} from '#/lib/hooks/usePalette'
import {colors, s} from '#/lib/styles'
import {TextLink} from '../util/Link'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
export function HomeLoggedOutCTA() {
const pal = usePalette('default')
const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
const {requestSwitchToAccount} = useLoggedOutViewControls()
const showCreateAccount = React.useCallback(() => {
requestSwitchToAccount({requestedAccount: 'new'})
}, [requestSwitchToAccount])
const showSignIn = React.useCallback(() => {
requestSwitchToAccount({requestedAccount: 'none'})
}, [requestSwitchToAccount])
return (
<ScrollView style={styles.container} testID="loggedOutCTA">
<View style={[styles.hero, isMobile && styles.heroMobile]}>
<Text style={[styles.title, pal.link]}>
<Trans>Bluesky</Trans>
</Text>
<Text
style={[
styles.subtitle,
isMobile && styles.subtitleMobile,
pal.textLight,
]}>
<Trans>See what's next</Trans>
</Text>
</View>
<View
testID="signinOrCreateAccount"
style={isMobile ? undefined : styles.btnsDesktop}>
<TouchableOpacity
testID="createAccountButton"
style={[
styles.btn,
isMobile && styles.btnMobile,
{backgroundColor: colors.blue3},
]}
onPress={showCreateAccount}
accessibilityRole="button"
accessibilityLabel={_(msg`Create new account`)}
accessibilityHint="Opens flow to create a new Bluesky account">
<Text
style={[
s.white,
styles.btnLabel,
isMobile && styles.btnLabelMobile,
]}>
<Trans>Create a new account</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
testID="signInButton"
style={[styles.btn, isMobile && styles.btnMobile, pal.btn]}
onPress={showSignIn}
accessibilityRole="button"
accessibilityLabel={_(msg`Sign in`)}
accessibilityHint="Opens flow to sign into your existing Bluesky account">
<Text
style={[
pal.text,
styles.btnLabel,
isMobile && styles.btnLabelMobile,
]}>
<Trans>Sign In</Trans>
</Text>
</TouchableOpacity>
</View>
<View style={[styles.footer, pal.view, pal.border]}>
<TextLink
type="2xl"
href="https://blueskyweb.xyz"
text={_(msg`Business`)}
style={[styles.footerLink, pal.link]}
/>
<TextLink
type="2xl"
href="https://blueskyweb.xyz/blog"
text={_(msg`Blog`)}
style={[styles.footerLink, pal.link]}
/>
<TextLink
type="2xl"
href="https://blueskyweb.xyz/join"
text={_(msg`Jobs`)}
style={[styles.footerLink, pal.link]}
/>
</View>
</ScrollView>
)
}
const styles = StyleSheet.create({
container: {
height: '100%',
},
hero: {
justifyContent: 'center',
paddingTop: 100,
paddingBottom: 30,
},
heroMobile: {
paddingBottom: 50,
},
title: {
textAlign: 'center',
fontSize: 68,
fontWeight: 'bold',
},
subtitle: {
textAlign: 'center',
fontSize: 48,
fontWeight: 'bold',
},
subtitleMobile: {
fontSize: 42,
},
btnsDesktop: {
flexDirection: 'row',
justifyContent: 'center',
gap: 20,
marginHorizontal: 20,
},
btn: {
borderRadius: 32,
width: 230,
paddingVertical: 12,
marginBottom: 20,
},
btnMobile: {
flex: 1,
width: 'auto',
marginHorizontal: 20,
paddingVertical: 16,
},
btnLabel: {
textAlign: 'center',
fontSize: 18,
},
btnLabelMobile: {
textAlign: 'center',
fontSize: 21,
},
footer: {
flexDirection: 'row',
gap: 20,
justifyContent: 'center',
},
footerLink: {},
})
+3 -1
View File
@@ -33,7 +33,9 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
const {requestedAccountSwitchTo} = useLoggedOutView()
const [screenState, setScreenState] = React.useState<ScreenState>(
requestedAccountSwitchTo
? ScreenState.S_Login
? requestedAccountSwitchTo === 'new'
? ScreenState.S_CreateAccount
: ScreenState.S_Login
: ScreenState.S_LoginOrCreateAccount,
)
const {isMobile} = useWebMediaQueries()
+11 -5
View File
@@ -7,6 +7,8 @@ import {usePalette} from 'lib/hooks/usePalette'
import {CenteredView} from '../util/Views'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype'
export const SplashScreen = ({
onPressSignin,
@@ -22,11 +24,14 @@ export const SplashScreen = ({
<CenteredView style={[styles.container, pal.view]}>
<ErrorBoundary>
<View style={styles.hero}>
<Text style={[styles.title, pal.link]}>
<Trans>Bluesky</Trans>
</Text>
<Text style={[styles.subtitle, pal.textLight]}>
<Trans>See what's next</Trans>
<Logo width={92} fill="sky" />
<View style={{paddingTop: 40, paddingBottom: 6}}>
<Logotype width={161} fill={pal.text.color} />
</View>
<Text type="lg-medium" style={[pal.textLight]}>
<Trans>What's up?</Trans>
</Text>
</View>
<View testID="signinOrCreateAccount" style={styles.btns}>
@@ -65,6 +70,7 @@ const styles = StyleSheet.create({
hero: {
flex: 2,
justifyContent: 'center',
alignItems: 'center',
},
btns: {
paddingBottom: 40,
+10 -10
View File
@@ -10,6 +10,8 @@ import {CenteredView} from '../util/Views'
import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Trans} from '@lingui/macro'
import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype'
export const SplashScreen = ({
onDismiss,
@@ -55,14 +57,15 @@ export const SplashScreen = ({
styles.containerInner,
isMobileWeb && styles.containerInnerMobile,
pal.border,
{alignItems: 'center'},
]}>
<ErrorBoundary>
<Text style={isMobileWeb ? styles.titleMobile : styles.title}>
Bluesky
</Text>
<Text style={isMobileWeb ? styles.subtitleMobile : styles.subtitle}>
See what's next
</Text>
<Logo width={92} fill="sky" />
<View style={{paddingTop: 40, paddingBottom: 20}}>
<Logotype width={161} fill={pal.text.color} />
</View>
<View testID="signinOrCreateAccount" style={styles.btns}>
<TouchableOpacity
testID="createAccountButton"
@@ -117,8 +120,6 @@ function Footer({styles}: {styles: ReturnType<typeof useStyles>}) {
)
}
const useStyles = () => {
const {isTabletOrMobile} = useWebMediaQueries()
const isMobileWeb = isWeb && isTabletOrMobile
return StyleSheet.create({
container: {
height: '100%',
@@ -161,8 +162,7 @@ const useStyles = () => {
paddingBottom: 30,
},
btns: {
flexDirection: isMobileWeb ? 'column' : 'row',
gap: 20,
gap: 10,
justifyContent: 'center',
paddingBottom: 40,
},
+6
View File
@@ -62,6 +62,7 @@ import {useProfileQuery} from '#/state/queries/profile'
import {useComposerControls} from '#/state/shell/composer'
import {emitPostCreated} from '#/state/events'
import {ThreadgateSetting} from '#/state/queries/threadgate'
import {logger} from '#/logger'
type Props = ComposerOpts
export const ComposePost = observer(function ComposePost({
@@ -228,6 +229,11 @@ export const ComposePost = observer(function ComposePost({
})
).uri
} catch (e: any) {
logger.error(e, {
message: `Composer: create post failed`,
hasImages: gallery.size > 0,
})
if (extLink) {
setExtLink({
...extLink,
+26 -22
View File
@@ -7,13 +7,15 @@ import {useNavigation} from '@react-navigation/native'
import {useAnalytics} from 'lib/analytics/analytics'
import {useQueryClient} from '@tanstack/react-query'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
import {MainScrollProvider} from '../util/MainScrollProvider'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useSetMinimalShellMode} from '#/state/shell'
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
import {ComposeIcon2} from 'lib/icons'
import {colors, s} from 'lib/styles'
import {FlatList, View, useWindowDimensions} from 'react-native'
import {View, useWindowDimensions} from 'react-native'
import {ListMethods} from '../util/List'
import {Feed} from '../posts/Feed'
import {TextLink} from '../util/Link'
import {FAB} from '../util/fab/FAB'
@@ -27,7 +29,7 @@ import {truncateAndInvalidate} from '#/state/queries/util'
import {TabState, getTabState, getRootNavigation} from '#/lib/routes/helpers'
import {isNative} from '#/platform/detection'
const POLL_FREQ = 30e3 // 30sec
const POLL_FREQ = 60e3 // 60sec
export function FeedPage({
testID,
@@ -51,10 +53,11 @@ export function FeedPage({
const {isDesktop} = useWebMediaQueries()
const queryClient = useQueryClient()
const {openComposer} = useComposerControls()
const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll()
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const setMinimalShellMode = useSetMinimalShellMode()
const {screen, track} = useAnalytics()
const headerOffset = useHeaderOffset()
const scrollElRef = React.useRef<FlatList>(null)
const scrollElRef = React.useRef<ListMethods>(null)
const [hasNew, setHasNew] = React.useState(false)
const scrollToTop = React.useCallback(() => {
@@ -62,8 +65,8 @@ export function FeedPage({
animated: isNative,
offset: -headerOffset,
})
resetMainScroll()
}, [headerOffset, resetMainScroll])
setMinimalShellMode(false)
}, [headerOffset, setMinimalShellMode])
const onSoftReset = React.useCallback(() => {
const isScreenFocused =
@@ -164,21 +167,22 @@ export function FeedPage({
return (
<View testID={testID} style={s.h100pct}>
<Feed
testID={testID ? `${testID}-feed` : undefined}
enabled={isPageFocused}
feed={feed}
feedParams={feedParams}
pollInterval={POLL_FREQ}
scrollElRef={scrollElRef}
onScroll={onMainScroll}
onHasNew={setHasNew}
scrollEventThrottle={1}
renderEmptyState={renderEmptyState}
renderEndOfFeed={renderEndOfFeed}
ListHeaderComponent={ListHeaderComponent}
headerOffset={headerOffset}
/>
<MainScrollProvider>
<Feed
testID={testID ? `${testID}-feed` : undefined}
enabled={isPageFocused}
feed={feed}
feedParams={feedParams}
pollInterval={POLL_FREQ}
scrollElRef={scrollElRef}
onScrolledDownChange={setIsScrolledDown}
onHasNew={setHasNew}
renderEmptyState={renderEmptyState}
renderEndOfFeed={renderEndOfFeed}
ListHeaderComponent={ListHeaderComponent}
headerOffset={headerOffset}
/>
</MainScrollProvider>
{(isScrolledDown || hasNew) && (
<LoadLatestBtn
onPress={onPressLoadLatest}
+5 -21
View File
@@ -1,4 +1,4 @@
import React, {MutableRefObject} from 'react'
import React from 'react'
import {
Dimensions,
RefreshControl,
@@ -8,18 +8,16 @@ import {
ViewStyle,
} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {FlatList} from '../util/Views'
import {List, ListRef} from '../util/List'
import {FeedSourceCardLoaded} from './FeedSourceCard'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {useProfileFeedgensQuery, RQKEY} from '#/state/queries/profile-feedgens'
import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll'
import {logger} from '#/logger'
import {Trans} from '@lingui/macro'
import {cleanError} from '#/lib/strings/errors'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useTheme} from '#/lib/ThemeContext'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {hydrateFeedGenerator} from '#/state/queries/feed'
@@ -37,9 +35,7 @@ interface SectionRef {
interface ProfileFeedgensProps {
did: string
scrollElRef: MutableRefObject<FlatList<any> | null>
onScroll?: OnScrollHandler
scrollEventThrottle?: number
scrollElRef: ListRef
headerOffset: number
enabled?: boolean
style?: StyleProp<ViewStyle>
@@ -50,16 +46,7 @@ export const ProfileFeedgens = React.forwardRef<
SectionRef,
ProfileFeedgensProps
>(function ProfileFeedgensImpl(
{
did,
scrollElRef,
onScroll,
scrollEventThrottle,
headerOffset,
enabled,
style,
testID,
},
{did, scrollElRef, headerOffset, enabled, style, testID},
ref,
) {
const pal = usePalette('default')
@@ -185,10 +172,9 @@ export const ProfileFeedgens = React.forwardRef<
[error, refetch, onPressRetryLoadMore, pal, preferences],
)
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
return (
<View testID={testID} style={style}>
<FlatList
<List
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={items}
@@ -207,8 +193,6 @@ export const ProfileFeedgens = React.forwardRef<
minHeight: Dimensions.get('window').height * 1.5,
}}
style={{paddingTop: headerOffset}}
onScroll={onScroll != null ? scrollHandler : undefined}
scrollEventThrottle={scrollEventThrottle}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
+7 -13
View File
@@ -1,4 +1,4 @@
import React, {MutableRefObject} from 'react'
import React from 'react'
import {
ActivityIndicator,
Dimensions,
@@ -8,7 +8,7 @@ import {
ViewStyle,
} from 'react-native'
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
import {FlatList} from '../util/Views'
import {List, ListRef} from '../util/List'
import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
@@ -18,10 +18,8 @@ import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useListMembersQuery} from '#/state/queries/list-members'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useSession} from '#/state/session'
import {cleanError} from '#/lib/strings/errors'
@@ -34,24 +32,22 @@ export function ListMembers({
list,
style,
scrollElRef,
onScroll,
onScrolledDownChange,
onPressTryAgain,
renderHeader,
renderEmptyState,
testID,
scrollEventThrottle,
headerOffset = 0,
desktopFixedHeightOffset,
}: {
list: string
style?: StyleProp<ViewStyle>
scrollElRef?: MutableRefObject<FlatList<any> | null>
onScroll: OnScrollHandler
scrollElRef?: ListRef
onScrolledDownChange: (isScrolledDown: boolean) => void
onPressTryAgain?: () => void
renderHeader: () => JSX.Element
renderEmptyState: () => JSX.Element
testID?: string
scrollEventThrottle?: number
headerOffset?: number
desktopFixedHeightOffset?: number
}) {
@@ -209,10 +205,9 @@ export function ListMembers({
[isFetching],
)
const scrollHandler = useAnimatedScrollHandler(onScroll)
return (
<View testID={testID} style={style}>
<FlatList
<List
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={items}
@@ -233,10 +228,9 @@ export function ListMembers({
minHeight: Dimensions.get('window').height * 1.5,
}}
style={{paddingTop: headerOffset}}
onScroll={scrollHandler}
onScrolledDownChange={onScrolledDownChange}
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
scrollEventThrottle={scrollEventThrottle}
removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
// @ts-ignore our .web version only -prf
+2 -2
View File
@@ -15,7 +15,7 @@ import {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {FlatList} from '../util/Views'
import {List} from '../util/List'
import {s} from 'lib/styles'
import {logger} from '#/logger'
import {Trans} from '@lingui/macro'
@@ -119,7 +119,7 @@ export function MyLists({
[error, onRefresh, renderItem, pal],
)
const FlatListCom = inline ? RNFlatList : FlatList
const FlatListCom = inline ? RNFlatList : List
return (
<View testID={testID} style={style}>
{items.length > 0 && (
+5 -21
View File
@@ -1,4 +1,4 @@
import React, {MutableRefObject} from 'react'
import React from 'react'
import {
Dimensions,
RefreshControl,
@@ -8,7 +8,7 @@ import {
ViewStyle,
} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {FlatList} from '../util/Views'
import {List, ListRef} from '../util/List'
import {ListCard} from './ListCard'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
@@ -16,11 +16,9 @@ import {Text} from '../util/text/Text'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useProfileListsQuery, RQKEY} from '#/state/queries/profile-lists'
import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll'
import {logger} from '#/logger'
import {Trans} from '@lingui/macro'
import {cleanError} from '#/lib/strings/errors'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useTheme} from '#/lib/ThemeContext'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {isNative} from '#/platform/detection'
@@ -36,9 +34,7 @@ interface SectionRef {
interface ProfileListsProps {
did: string
scrollElRef: MutableRefObject<FlatList<any> | null>
onScroll?: OnScrollHandler
scrollEventThrottle?: number
scrollElRef: ListRef
headerOffset: number
enabled?: boolean
style?: StyleProp<ViewStyle>
@@ -47,16 +43,7 @@ interface ProfileListsProps {
export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
function ProfileListsImpl(
{
did,
scrollElRef,
onScroll,
scrollEventThrottle,
headerOffset,
enabled,
style,
testID,
},
{did, scrollElRef, headerOffset, enabled, style, testID},
ref,
) {
const pal = usePalette('default')
@@ -187,10 +174,9 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
[error, refetch, onPressRetryLoadMore, pal],
)
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
return (
<View testID={testID} style={style}>
<FlatList
<List
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={items}
@@ -209,8 +195,6 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
minHeight: Dimensions.get('window').height * 1.5,
}}
style={{paddingTop: headerOffset}}
onScroll={onScroll != null ? scrollHandler : undefined}
scrollEventThrottle={scrollEventThrottle}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
+8 -11
View File
@@ -1,13 +1,11 @@
import React, {MutableRefObject} from 'react'
import {CenteredView, FlatList} from '../util/Views'
import React from 'react'
import {CenteredView} from '../util/Views'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
import {FeedItem} from './FeedItem'
import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {EmptyState} from '../util/EmptyState'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
@@ -15,6 +13,7 @@ import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread'
import {logger} from '#/logger'
import {cleanError} from '#/lib/strings/errors'
import {useModerationOpts} from '#/state/queries/preferences'
import {List, ListRef} from '../util/List'
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
@@ -23,12 +22,12 @@ const LOADING_ITEM = {_reactKey: '__loading__'}
export function Feed({
scrollElRef,
onPressTryAgain,
onScroll,
onScrolledDownChange,
ListHeaderComponent,
}: {
scrollElRef?: MutableRefObject<FlatList<any> | null>
scrollElRef?: ListRef
onPressTryAgain?: () => void
onScroll?: OnScrollHandler
onScrolledDownChange: (isScrolledDown: boolean) => void
ListHeaderComponent?: () => JSX.Element
}) {
const pal = usePalette('default')
@@ -135,7 +134,6 @@ export function Feed({
[isFetchingNextPage],
)
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
return (
<View style={s.hContentRegion}>
{error && (
@@ -146,7 +144,7 @@ export function Feed({
/>
</CenteredView>
)}
<FlatList
<List
testID="notifsFeed"
ref={scrollElRef}
data={items}
@@ -164,8 +162,7 @@ export function Feed({
}
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
onScroll={scrollHandler}
scrollEventThrottle={1}
onScrolledDownChange={onScrolledDownChange}
contentContainerStyle={s.contentContainer}
// @ts-ignore our .web version only -prf
desktopFixedHeight
+5 -8
View File
@@ -3,12 +3,9 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {TabBar} from 'view/com/pager/TabBar'
import {RenderTabBarFnProps} from 'view/com/pager/Pager'
import {usePalette} from 'lib/hooks/usePalette'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {s} from 'lib/styles'
import {HITSLOP_10} from 'lib/constants'
import Animated from 'react-native-reanimated'
import {msg} from '@lingui/macro'
@@ -21,17 +18,17 @@ import {usePinnedFeedsInfos} from '#/state/queries/feed'
import {isWeb} from 'platform/detection'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
import {Logo} from '#/view/icons/Logo'
export function FeedsTabBar(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
) {
const pal = usePalette('default')
const {isSandbox, hasSession} = useSession()
const {hasSession} = useSession()
const {_} = useLingui()
const setDrawerOpen = useSetDrawerOpen()
const navigation = useNavigation<NavigationProp>()
const {feeds, hasPinnedCustom} = usePinnedFeedsInfos()
const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3)
const {headerHeight} = useShellLayout()
const {headerMinimalShellTransform} = useMinimalShellMode()
const pinnedDisplayNames = hasSession ? feeds.map(f => f.displayName) : []
@@ -86,9 +83,9 @@ export function FeedsTabBar(
/>
</TouchableOpacity>
</View>
<Text style={[brandBlue, s.bold, styles.title]}>
{isSandbox ? 'SANDBOX' : 'Bluesky'}
</Text>
<View>
<Logo width={30} />
</View>
<View style={[pal.view, {width: 18}]}>
{hasSession && (
<Link
+14 -33
View File
@@ -1,7 +1,6 @@
import * as React from 'react'
import {
LayoutChangeEvent,
FlatList,
ScrollView,
StyleSheet,
View,
@@ -20,17 +19,14 @@ import Animated, {
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {TabBar} from './TabBar'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
const SCROLLED_DOWN_LIMIT = 200
import {ListMethods} from '../util/List'
import {ScrollProvider} from '#/lib/ScrollContext'
export interface PagerWithHeaderChildParams {
headerHeight: number
isFocused: boolean
onScroll: OnScrollHandler
isScrolledDown: boolean
scrollElRef: React.MutableRefObject<FlatList<any> | ScrollView | null>
scrollElRef: React.MutableRefObject<ListMethods | ScrollView | null>
}
export interface PagerWithHeaderProps {
@@ -62,7 +58,6 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
const [currentPage, setCurrentPage] = React.useState(0)
const [tabBarHeight, setTabBarHeight] = React.useState(0)
const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const scrollY = useSharedValue(0)
const headerHeight = headerOnlyHeight + tabBarHeight
@@ -155,15 +150,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
if (!throttleTimeout.current) {
throttleTimeout.current = setTimeout(() => {
throttleTimeout.current = null
runOnUI(adjustScrollForOtherPages)()
const nextIsScrolledDown = scrollY.value > SCROLLED_DOWN_LIMIT
if (isScrolledDown !== nextIsScrolledDown) {
React.startTransition(() => {
setIsScrolledDown(nextIsScrolledDown)
})
}
}, 80 /* Sync often enough you're unlikely to catch it unsynced */)
}
})
@@ -211,7 +198,6 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
index={i}
isReady={isReady}
isFocused={i === currentPage}
isScrolledDown={isScrolledDown}
onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
registerRef={registerRef}
renderTab={child}
@@ -293,7 +279,6 @@ function PagerItem({
index,
isReady,
isFocused,
isScrolledDown,
onScrollWorklet,
renderTab,
registerRef,
@@ -302,7 +287,6 @@ function PagerItem({
index: number
isFocused: boolean
isReady: boolean
isScrolledDown: boolean
registerRef: (scrollRef: AnimatedRef<any> | null, atIndex: number) => void
onScrollWorklet: (e: NativeScrollEvent) => void
renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null
@@ -316,24 +300,21 @@ function PagerItem({
}
}, [scrollElRef, registerRef, index])
const scrollHandler = React.useMemo(
() => ({onScroll: onScrollWorklet}),
[onScrollWorklet],
)
if (!isReady || renderTab == null) {
return null
}
return renderTab({
headerHeight,
isFocused,
isScrolledDown,
onScroll: scrollHandler,
scrollElRef: scrollElRef as React.MutableRefObject<
FlatList<any> | ScrollView | null
>,
})
return (
<ScrollProvider onScroll={onScrollWorklet}>
{renderTab({
headerHeight,
isFocused,
scrollElRef: scrollElRef as React.MutableRefObject<
ListMethods | ScrollView | null
>,
})}
</ScrollProvider>
)
}
const styles = StyleSheet.create({
+3 -2
View File
@@ -1,7 +1,8 @@
import React, {useCallback, useMemo, useState} from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {CenteredView, FlatList} from '../util/Views'
import {CenteredView} from '../util/Views'
import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
@@ -84,7 +85,7 @@ export function PostLikedBy({uri}: {uri: string}) {
// loaded
// =
return (
<FlatList
<List
data={likes}
keyExtractor={item => item.actor.did}
refreshControl={
+3 -2
View File
@@ -1,7 +1,8 @@
import React, {useMemo, useCallback, useState} from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {CenteredView, FlatList} from '../util/Views'
import {CenteredView} from '../util/Views'
import {List} from '../util/List'
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {usePalette} from 'lib/hooks/usePalette'
@@ -85,7 +86,7 @@ export function PostRepostedBy({uri}: {uri: string}) {
// loaded
// =
return (
<FlatList
<List
data={repostedBy}
keyExtractor={item => item.did}
refreshControl={
+18 -7
View File
@@ -8,7 +8,8 @@ import {
View,
} from 'react-native'
import {AppBskyFeedDefs} from '@atproto/api'
import {CenteredView, FlatList} from '../util/Views'
import {CenteredView} from '../util/Views'
import {List, ListMethods} from '../util/List'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@@ -140,7 +141,7 @@ function PostThreadLoaded({
const {_} = useLingui()
const pal = usePalette('default')
const {isTablet, isDesktop} = useWebMediaQueries()
const ref = useRef<FlatList>(null)
const ref = useRef<ListMethods>(null)
const highlightedPostRef = useRef<View | null>(null)
const needsScrollAdjustment = useRef<boolean>(
!isNative || // web always uses scroll adjustment
@@ -156,7 +157,9 @@ function PostThreadLoaded({
// construct content
const posts = React.useMemo(() => {
let arr = [TOP_COMPONENT].concat(
Array.from(flattenThreadSkeleton(sortThread(thread, threadViewPrefs))),
Array.from(
flattenThreadSkeleton(sortThread(thread, threadViewPrefs), hasSession),
),
)
if (arr.length > maxVisible) {
arr = arr.slice(0, maxVisible).concat([LOAD_MORE])
@@ -165,7 +168,7 @@ function PostThreadLoaded({
arr.push(BOTTOM_COMPONENT)
}
return arr
}, [thread, maxVisible, threadViewPrefs])
}, [thread, maxVisible, threadViewPrefs, hasSession])
/**
* NOTE
@@ -335,7 +338,7 @@ function PostThreadLoaded({
)
return (
<FlatList
<List
ref={ref}
data={posts}
initialNumToRender={!isNative ? posts.length : undefined}
@@ -467,20 +470,24 @@ function isThreadPost(v: unknown): v is ThreadPost {
function* flattenThreadSkeleton(
node: ThreadNode,
hasSession: boolean,
): Generator<YieldedItem, void> {
if (node.type === 'post') {
if (node.parent) {
yield* flattenThreadSkeleton(node.parent)
yield* flattenThreadSkeleton(node.parent, hasSession)
} else if (node.ctx.isParentLoading) {
yield PARENT_SPINNER
}
if (!hasSession && node.ctx.depth > 0 && hasPwiOptOut(node)) {
return
}
yield node
if (node.ctx.isHighlightedPost && !node.post.viewer?.replyDisabled) {
yield REPLY_PROMPT
}
if (node.replies?.length) {
for (const reply of node.replies) {
yield* flattenThreadSkeleton(reply)
yield* flattenThreadSkeleton(reply, hasSession)
}
} else if (node.ctx.isChildLoading) {
yield CHILD_SPINNER
@@ -492,6 +499,10 @@ function* flattenThreadSkeleton(
}
}
function hasPwiOptOut(node: ThreadPost) {
return !!node.post.author.labels?.find(l => l.val === '!no-unauthenticated')
}
function hasBranchingReplies(node: ThreadNode) {
if (node.type !== 'post') {
return false
+4 -1
View File
@@ -187,6 +187,9 @@ let PostThreadItemLoaded = ({
return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
}, [post.uri, post.author])
const repostsTitle = 'Reposts of this post'
const isSelfLabeledPost =
moderation.decisions.post.cause?.type === 'label' &&
moderation.decisions.post.cause.label.src === currentAccount?.did
const translatorUrl = getTranslatorLink(
record?.text || '',
@@ -351,7 +354,7 @@ let PostThreadItemLoaded = ({
includeMute
style={styles.alert}
/>
{post.author.did === currentAccount?.did ? (
{post.author.did === currentAccount?.did && !isSelfLabeledPost ? (
<LabelInfo
details={{uri: post.uri, cid: post.cid}}
labels={post.labels}
+29 -17
View File
@@ -1,4 +1,4 @@
import React, {memo, MutableRefObject} from 'react'
import React, {memo} from 'react'
import {
ActivityIndicator,
AppState,
@@ -10,15 +10,13 @@ import {
ViewStyle,
} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {FlatList} from '../util/Views'
import {List, ListRef} from '../util/List'
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {FeedErrorMessage} from './FeedErrorMessage'
import {FeedSlice} from './FeedSlice'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useTheme} from 'lib/ThemeContext'
import {logger} from '#/logger'
import {
@@ -31,12 +29,16 @@ import {
import {isWeb} from '#/platform/detection'
import {listenPostCreated} from '#/state/events'
import {useSession} from '#/state/session'
import {STALE} from '#/state/queries'
const LOADING_ITEM = {_reactKey: '__loading__'}
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const ERROR_ITEM = {_reactKey: '__error__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
const REFRESH_AFTER = STALE.HOURS.ONE
const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY
let Feed = ({
feed,
feedParams,
@@ -45,9 +47,8 @@ let Feed = ({
enabled,
pollInterval,
scrollElRef,
onScroll,
onScrolledDownChange,
onHasNew,
scrollEventThrottle,
renderEmptyState,
renderEndOfFeed,
testID,
@@ -62,10 +63,9 @@ let Feed = ({
style?: StyleProp<ViewStyle>
enabled?: boolean
pollInterval?: number
scrollElRef?: MutableRefObject<FlatList<any> | null>
scrollElRef?: ListRef
onHasNew?: (v: boolean) => void
onScroll?: OnScrollHandler
scrollEventThrottle?: number
onScrolledDownChange?: (isScrolledDown: boolean) => void
renderEmptyState: () => JSX.Element
renderEndOfFeed?: () => JSX.Element
testID?: string
@@ -81,6 +81,7 @@ let Feed = ({
const {currentAccount} = useSession()
const [isPTRing, setIsPTRing] = React.useState(false)
const checkForNewRef = React.useRef<(() => void) | null>(null)
const lastFetchRef = React.useRef<number>(Date.now())
const opts = React.useMemo(
() => ({enabled, ignoreFilterFor}),
@@ -98,6 +99,9 @@ let Feed = ({
fetchNextPage,
} = usePostFeedQuery(feed, feedParams, opts)
const isEmpty = !isFetching && !data?.pages[0]?.slices.length
if (data?.pages[0]) {
lastFetchRef.current = data?.pages[0].fetchedAt
}
const checkForNew = React.useCallback(async () => {
if (!data?.pages[0] || isFetching || !onHasNew || !enabled) {
@@ -137,11 +141,21 @@ let Feed = ({
checkForNewRef.current = checkForNew
}, [checkForNew])
React.useEffect(() => {
if (enabled && checkForNewRef.current) {
// check for new on enable (aka on focus)
checkForNewRef.current()
if (enabled) {
const timeSinceFirstLoad = Date.now() - lastFetchRef.current
if (timeSinceFirstLoad > REFRESH_AFTER) {
// do a full refresh
scrollElRef?.current?.scrollToOffset({offset: 0, animated: false})
queryClient.resetQueries({queryKey: RQKEY(feed)})
} else if (
timeSinceFirstLoad > CHECK_LATEST_AFTER &&
checkForNewRef.current
) {
// check for new on enable (aka on focus)
checkForNewRef.current()
}
}
}, [enabled])
}, [enabled, feed, queryClient, scrollElRef])
React.useEffect(() => {
let cleanup1: () => void | undefined, cleanup2: () => void | undefined
const subscription = AppState.addEventListener('change', nextAppState => {
@@ -270,10 +284,9 @@ let Feed = ({
)
}, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset])
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
return (
<View testID={testID} style={style}>
<FlatList
<List
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={feedItems}
@@ -294,8 +307,7 @@ let Feed = ({
minHeight: Dimensions.get('window').height * 1.5,
}}
style={{paddingTop: headerOffset}}
onScroll={onScroll != null ? scrollHandler : undefined}
scrollEventThrottle={scrollEventThrottle}
onScrolledDownChange={onScrolledDownChange}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
onEndReached={onEndReached}
onEndReachedThreshold={2} // number of posts left to trigger load more
+3
View File
@@ -50,6 +50,9 @@ export function ProfileCard({
return null
}
const moderation = moderateProfile(profile, moderationOpts)
if (moderation.account.filter) {
return null
}
return (
<Link
+3 -2
View File
@@ -1,7 +1,8 @@
import React from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {CenteredView, FlatList} from '../util/Views'
import {CenteredView} from '../util/Views'
import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from './ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
@@ -86,7 +87,7 @@ export function ProfileFollowers({name}: {name: string}) {
// loaded
// =
return (
<FlatList
<List
data={followers}
keyExtractor={item => item.did}
refreshControl={
+3 -2
View File
@@ -1,7 +1,8 @@
import React from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {CenteredView, FlatList} from '../util/Views'
import {CenteredView} from '../util/Views'
import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from './ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
@@ -86,7 +87,7 @@ export function ProfileFollows({name}: {name: string}) {
// loaded
// =
return (
<FlatList
<List
data={follows}
keyExtractor={item => item.did}
refreshControl={
+64
View File
@@ -0,0 +1,64 @@
import React, {memo, startTransition} from 'react'
import {FlatListProps} from 'react-native'
import {FlatList_INTERNAL} from './Views'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {runOnJS, useSharedValue} from 'react-native-reanimated'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
export type ListMethods = FlatList_INTERNAL
export type ListProps<ItemT> = Omit<
FlatListProps<ItemT>,
'onScroll' // Use ScrollContext instead.
> & {
onScrolledDownChange?: (isScrolledDown: boolean) => void
}
export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
const SCROLLED_DOWN_LIMIT = 200
function ListImpl<ItemT>(
{onScrolledDownChange, ...props}: ListProps<ItemT>,
ref: React.Ref<ListMethods>,
) {
const isScrolledDown = useSharedValue(false)
const contextScrollHandlers = useScrollHandlers()
function handleScrolledDownChange(didScrollDown: boolean) {
startTransition(() => {
onScrolledDownChange?.(didScrollDown)
})
}
const scrollHandler = useAnimatedScrollHandler({
onBeginDrag(e, ctx) {
contextScrollHandlers.onBeginDrag?.(e, ctx)
},
onEndDrag(e, ctx) {
contextScrollHandlers.onEndDrag?.(e, ctx)
},
onScroll(e, ctx) {
contextScrollHandlers.onScroll?.(e, ctx)
const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT
if (isScrolledDown.value !== didScrollDown) {
isScrolledDown.value = didScrollDown
if (onScrolledDownChange != null) {
runOnJS(handleScrolledDownChange)(didScrollDown)
}
}
},
})
return (
<FlatList_INTERNAL
{...props}
onScroll={scrollHandler}
scrollEventThrottle={1}
ref={ref}
/>
)
}
export const List = memo(React.forwardRef(ListImpl)) as <ItemT>(
props: ListProps<ItemT> & {ref?: React.Ref<ListMethods>},
) => React.ReactElement
+97
View File
@@ -0,0 +1,97 @@
import React, {useCallback} from 'react'
import {ScrollProvider} from '#/lib/ScrollContext'
import {NativeScrollEvent} from 'react-native'
import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell'
import {useShellLayout} from '#/state/shell/shell-layout'
import {isWeb} from 'platform/detection'
import {useSharedValue, interpolate} from 'react-native-reanimated'
function clamp(num: number, min: number, max: number) {
'worklet'
return Math.min(Math.max(num, min), max)
}
export function MainScrollProvider({children}: {children: React.ReactNode}) {
const {headerHeight} = useShellLayout()
const mode = useMinimalShellMode()
const setMode = useSetMinimalShellMode()
const startDragOffset = useSharedValue<number | null>(null)
const startMode = useSharedValue<number | null>(null)
const onBeginDrag = useCallback(
(e: NativeScrollEvent) => {
'worklet'
startDragOffset.value = e.contentOffset.y
startMode.value = mode.value
},
[mode, startDragOffset, startMode],
)
const onEndDrag = useCallback(
(e: NativeScrollEvent) => {
'worklet'
startDragOffset.value = null
startMode.value = null
if (e.contentOffset.y < headerHeight.value / 2) {
// If we're close to the top, show the shell.
setMode(false)
} else {
// Snap to whichever state is the closest.
setMode(Math.round(mode.value) === 1)
}
},
[startDragOffset, startMode, setMode, mode, headerHeight],
)
const onScroll = useCallback(
(e: NativeScrollEvent) => {
'worklet'
if (startDragOffset.value === null || startMode.value === null) {
if (mode.value !== 0 && e.contentOffset.y < headerHeight.value) {
// If we're close enough to the top, always show the shell.
// Even if we're not dragging.
setMode(false)
return
}
if (isWeb) {
// On the web, there is no concept of "starting" the drag.
// When we get the first scroll event, we consider that the start.
startDragOffset.value = e.contentOffset.y
startMode.value = mode.value
}
return
}
// The "mode" value is always between 0 and 1.
// Figure out how much to move it based on the current dragged distance.
const dy = e.contentOffset.y - startDragOffset.value
const dProgress = interpolate(
dy,
[-headerHeight.value, headerHeight.value],
[-1, 1],
)
const newValue = clamp(startMode.value + dProgress, 0, 1)
if (newValue !== mode.value) {
// Manually adjust the value. This won't be (and shouldn't be) animated.
mode.value = newValue
}
if (isWeb) {
// On the web, there is no concept of "starting" the drag,
// so we don't have any specific anchor point to calculate the distance.
// Instead, update it continuosly along the way and diff with the last event.
startDragOffset.value = e.contentOffset.y
startMode.value = mode.value
}
},
[headerHeight, mode, setMode, startDragOffset, startMode],
)
return (
<ScrollProvider
onBeginDrag={onBeginDrag}
onEndDrag={onEndDrag}
onScroll={onScroll}>
{children}
</ScrollProvider>
)
}
+6 -5
View File
@@ -1,13 +1,14 @@
import React, {useEffect, useState} from 'react'
import {
NativeSyntheticEvent,
NativeScrollEvent,
Pressable,
RefreshControl,
StyleSheet,
View,
ScrollView,
} from 'react-native'
import {FlatList} from './Views'
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
import {FlatList_INTERNAL} from './Views'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {Text} from './text/Text'
import {usePalette} from 'lib/hooks/usePalette'
@@ -38,7 +39,7 @@ export const ViewSelector = React.forwardRef<
| null
| undefined
onSelectView?: (viewIndex: number) => void
onScroll?: OnScrollCb
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
onRefresh?: () => void
onEndReached?: (info: {distanceFromEnd: number}) => void
}
@@ -59,7 +60,7 @@ export const ViewSelector = React.forwardRef<
) {
const pal = usePalette('default')
const [selectedIndex, setSelectedIndex] = useState<number>(0)
const flatListRef = React.useRef<FlatList>(null)
const flatListRef = React.useRef<FlatList_INTERNAL>(null)
// events
// =
@@ -110,7 +111,7 @@ export const ViewSelector = React.forwardRef<
[items],
)
return (
<FlatList
<FlatList_INTERNAL
ref={flatListRef}
data={data}
keyExtractor={keyExtractor}
+1 -1
View File
@@ -1,6 +1,6 @@
import React from 'react'
import {ViewProps} from 'react-native'
export {FlatList, ScrollView} from 'react-native'
export {FlatList as FlatList_INTERNAL, ScrollView} from 'react-native'
export function CenteredView({
style,
sideBorders,
+1 -1
View File
@@ -2,7 +2,7 @@ import React from 'react'
import {View} from 'react-native'
import Animated from 'react-native-reanimated'
export const FlatList = Animated.FlatList
export const FlatList_INTERNAL = Animated.FlatList
export const ScrollView = Animated.ScrollView
export function CenteredView(props) {
return <View {...props} />
+1 -1
View File
@@ -49,7 +49,7 @@ export function CenteredView({
return <View style={style} {...props} />
}
export const FlatList = React.forwardRef(function FlatListImpl<ItemT>(
export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
{
contentContainerStyle,
style,
+12 -8
View File
@@ -7,7 +7,7 @@ import {Text} from '../text/Text'
import {ShieldExclamation} from 'lib/icons'
import {describeModerationCause} from 'lib/moderation'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
import {isPostMediaBlurred} from 'lib/moderation'
@@ -95,13 +95,17 @@ export function ContentHider({
<Text type="md" style={pal.text}>
{desc.name}
</Text>
{!moderation.noOverride && (
<View style={styles.showBtn}>
<Text type="lg" style={pal.link}>
{override ? 'Hide' : 'Show'}
</Text>
</View>
)}
<View style={styles.showBtn}>
<Text type="lg" style={pal.link}>
{moderation.noOverride ? (
<Trans>Learn more</Trans>
) : override ? (
<Trans>Hide</Trans>
) : (
<Trans>Show</Trans>
)}
</Text>
</View>
</Pressable>
{override && <View style={childContainerStyle}>{children}</View>}
</View>
+2 -1
View File
@@ -43,7 +43,8 @@ export function LabelInfo({
]}>
<Text type="sm" style={pal.text}>
<Trans>
This {'did' in details ? 'account' : 'post'} has been labeled.
A content warning has been applied to this{' '}
{'did' in details ? 'account' : 'post'}.
</Trans>{' '}
</Text>
<Pressable
+41 -23
View File
@@ -22,6 +22,7 @@ import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {s} from '#/lib/styles'
import {CenteredView} from '../Views'
export function ScreenHider({
testID,
@@ -53,41 +54,58 @@ export function ScreenHider({
)
}
const isNoPwi =
moderation.cause?.type === 'label' &&
moderation.cause?.labelDef.id === '!no-unauthenticated'
const desc = describeModerationCause(moderation.cause, 'account')
return (
<View style={[styles.container, pal.view, containerStyle]}>
<CenteredView
style={[styles.container, pal.view, containerStyle]}
sideBorders>
<View style={styles.iconContainer}>
<View style={[styles.icon, palInverted.view]}>
<FontAwesomeIcon
icon="exclamation"
icon={isNoPwi ? ['far', 'eye-slash'] : 'exclamation'}
style={pal.textInverted as FontAwesomeIconStyle}
size={24}
/>
</View>
</View>
<Text type="title-2xl" style={[styles.title, pal.text]}>
<Trans>Content Warning</Trans>
{isNoPwi ? (
<Trans>Sign-in Required</Trans>
) : (
<Trans>Content Warning</Trans>
)}
</Text>
<Text type="2xl" style={[styles.description, pal.textLight]}>
<Trans>This {screenDescription} has been flagged:</Trans>
<Text type="2xl-medium" style={[pal.text, s.ml5]}>
{desc.name}.
</Text>
<TouchableWithoutFeedback
onPress={() => {
openModal({
name: 'moderation-details',
context: 'account',
moderation,
})
}}
accessibilityRole="button"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint="">
<Text type="2xl" style={pal.link}>
<Trans>Learn More</Trans>
</Text>
</TouchableWithoutFeedback>
{isNoPwi ? (
<Trans>
This account has requested that users sign in to view their profile.
</Trans>
) : (
<>
<Trans>This {screenDescription} has been flagged:</Trans>
<Text type="2xl-medium" style={[pal.text, s.ml5]}>
{desc.name}.
</Text>
<TouchableWithoutFeedback
onPress={() => {
openModal({
name: 'moderation-details',
context: 'account',
moderation,
})
}}
accessibilityRole="button"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint="">
<Text type="2xl" style={pal.link}>
<Trans>Learn More</Trans>
</Text>
</TouchableWithoutFeedback>
</>
)}{' '}
</Text>
{isMobile && <View style={styles.spacer} />}
<View style={styles.btnContainer}>
@@ -116,7 +134,7 @@ export function ScreenHider({
</Button>
)}
</View>
</View>
</CenteredView>
)
}
+3 -6
View File
@@ -10,7 +10,7 @@ import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
import {Text} from '../text/Text'
import {PostDropdownBtn} from '../forms/PostDropdownBtn'
import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons'
import {s, colors} from 'lib/styles'
import {s} from 'lib/styles'
import {pluralize} from 'lib/strings/helpers'
import {useTheme} from 'lib/ThemeContext'
import {RepostButton} from './RepostButton'
@@ -180,7 +180,7 @@ let PostCtrls = ({
accessibilityHint=""
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
{post.viewer?.like ? (
<HeartIconSolid style={styles.ctrlIconLiked} size={big ? 22 : 16} />
<HeartIconSolid style={s.likeColor} size={big ? 22 : 16} />
) : (
<HeartIcon
style={[defaultCtrlColor, big ? styles.mt1 : undefined]}
@@ -193,7 +193,7 @@ let PostCtrls = ({
testID="likeCount"
style={
post.viewer?.like
? [s.bold, s.red3, s.f15, s.ml5]
? [s.bold, s.likeColor, s.f15, s.ml5]
: [defaultCtrlColor, s.f15, s.ml5]
}>
{post.likeCount}
@@ -233,9 +233,6 @@ const styles = StyleSheet.create({
paddingLeft: 5,
paddingRight: 5,
},
ctrlIconLiked: {
color: colors.like,
},
mt1: {
marginTop: 1,
},
+48
View File
@@ -0,0 +1,48 @@
import React from 'react'
import Svg, {
Path,
Defs,
LinearGradient,
Stop,
SvgProps,
PathProps,
} from 'react-native-svg'
import {colors} from '#/lib/styles'
const ratio = 57 / 64
type Props = {
fill?: PathProps['fill']
} & SvgProps
export const Logo = React.forwardRef(function LogoImpl(props: Props, ref) {
const {fill, ...rest} = props
const gradient = fill === 'sky'
const _fill = gradient ? 'url(#sky)' : fill || colors.blue3
// @ts-ignore it's fiiiiine
const size = parseInt(rest.width || 32)
return (
<Svg
fill="none"
// @ts-ignore it's fiiiiine
ref={ref}
viewBox="0 0 64 57"
{...rest}
style={{width: size, height: size * ratio}}>
{gradient && (
<Defs>
<LinearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<Stop offset="0" stopColor="#0A7AFF" stopOpacity="1" />
<Stop offset="1" stopColor="#59B9FF" stopOpacity="1" />
</LinearGradient>
</Defs>
)}
<Path
fill={_fill}
d="M13.873 3.805C21.21 9.332 29.103 20.537 32 26.55v15.882c0-.338-.13.044-.41.867-1.512 4.456-7.418 21.847-20.923 7.944-7.111-7.32-3.819-14.64 9.125-16.85-7.405 1.264-15.73-.825-18.014-9.015C1.12 23.022 0 8.51 0 6.55 0-3.268 8.579-.182 13.873 3.805ZM50.127 3.805C42.79 9.332 34.897 20.537 32 26.55v15.882c0-.338.13.044.41.867 1.512 4.456 7.418 21.847 20.923 7.944 7.111-7.32 3.819-14.64-9.125-16.85 7.405 1.264 15.73-.825 18.014-9.015C62.88 23.022 64 8.51 64 6.55c0-9.818-8.578-6.732-13.873-2.745Z"
/>
</Svg>
)
})
+29
View File
@@ -0,0 +1,29 @@
import React from 'react'
import Svg, {Path, SvgProps, PathProps} from 'react-native-svg'
import {usePalette} from '#/lib/hooks/usePalette'
const ratio = 17 / 64
export function Logotype({
fill,
...rest
}: {fill?: PathProps['fill']} & SvgProps) {
const pal = usePalette('default')
// @ts-ignore it's fiiiiine
const size = parseInt(rest.width || 32)
return (
<Svg
fill="none"
viewBox="0 0 64 17"
{...rest}
width={size}
height={Number(size) * ratio}>
<Path
fill={fill || pal.text.color}
d="M8.478 6.252c1.503.538 2.3 1.78 2.3 3.172 0 2.356-1.576 3.785-4.6 3.785H0V0h5.974c2.875 0 4.267 1.466 4.267 3.413 0 1.3-.594 2.245-1.763 2.839Zm-2.69-4.193H2.504v3.45h3.284c1.28 0 1.967-.667 1.967-1.78 0-1.02-.705-1.67-1.967-1.67Zm-3.284 9.072h3.544c1.41 0 2.17-.65 2.17-1.818 0-1.224-.723-1.837-2.17-1.837H2.504v3.655ZM14.251 13.209h-2.337V0h2.337v13.209ZM22.001 8.998V3.636h2.338v9.573h-2.263v-1.392c-.724 1.076-1.726 1.614-3.006 1.614-2.022 0-3.34-1.224-3.34-3.45V3.636h2.338v5.955c0 1.206.594 1.818 1.8 1.818 1.132 0 2.133-.835 2.133-2.411ZM34.979 8.59v.556h-7.161c.167 1.651 1.076 2.467 2.486 2.467 1.076 0 1.8-.463 2.189-1.372h2.244c-.5 1.947-2.17 3.19-4.452 3.19-1.428 0-2.579-.463-3.45-1.372-.872-.91-1.318-2.115-1.318-3.637 0-1.502.427-2.708 1.299-3.636.872-.909 2.004-1.372 3.432-1.372 1.447 0 2.597.482 3.45 1.428.854.946 1.28 2.208 1.28 3.747Zm-4.75-3.358c-1.28 0-2.17.742-2.393 2.281h4.805c-.204-1.391-1.057-2.281-2.411-2.281ZM40.16 13.469c-2.783 0-4.249-1.095-4.379-3.303h2.282c.13 1.188.724 1.633 2.134 1.633 1.261 0 1.892-.39 1.892-1.15 0-.687-.445-1.02-1.874-1.262l-1.094-.185c-2.097-.353-3.136-1.318-3.136-2.894 0-1.8 1.429-2.894 3.97-2.894 2.728 0 4.138 1.075 4.23 3.246h-2.207c-.056-1.169-.742-1.577-2.023-1.577-1.113 0-1.67.371-1.67 1.113 0 .668.483.965 1.596 1.169l1.206.186c2.32.426 3.32 1.28 3.32 2.912 0 1.93-1.557 3.006-4.247 3.006ZM54.667 13.209h-2.671l-2.783-4.453-1.447 1.447v3.006h-2.3V0h2.3v7.606l3.896-3.97h2.783l-3.618 3.618 3.84 5.955ZM60.772 6.048l.78-2.412H64l-3.692 10.352c-.39 1.057-.872 1.818-1.484 2.245-.612.426-1.484.63-2.634.63-.39 0-.724-.018-1.02-.055V14.97h.89c1.057 0 1.577-.65 1.577-1.54 0-.445-.149-1.094-.446-1.929l-2.746-7.866h2.487l.779 2.393c.575 1.8 1.076 3.58 1.521 5.343.408-1.521.928-3.302 1.54-5.324Z"
/>
</Svg>
)
}
+2 -2
View File
@@ -19,7 +19,7 @@ import {
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import debounce from 'lodash.debounce'
import {Text} from 'view/com/util/text/Text'
import {FlatList} from 'view/com/util/Views'
import {List} from 'view/com/util/List'
import {useFocusEffect} from '@react-navigation/native'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {Trans, msg} from '@lingui/macro'
@@ -481,7 +481,7 @@ export function FeedsScreen(_props: Props) {
{preferences ? <View /> : <ActivityIndicator />}
<FlatList
<List
style={[!isTabletOrDesktop && s.flex1, styles.list]}
data={items}
keyExtractor={item => item.key}
+2 -6
View File
@@ -9,6 +9,7 @@ import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
import {FeedsTabBar} from '../com/pager/FeedsTabBar'
import {Pager, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {FeedPage} from 'view/com/feeds/FeedPage'
import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA'
import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
@@ -199,12 +200,7 @@ function HomeScreenReady({
onPageScrollStateChanged={onPageScrollStateChanged}
renderTabBar={renderTabBar}
tabBarPosition="top">
<FeedPage
testID="customFeedPage"
isPageFocused={true}
feed={`feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot`}
renderEmptyState={renderCustomFeedEmptyState}
/>
<HomeLoggedOutCTA />
</Pager>
)
}
+14 -11
View File
@@ -1,5 +1,5 @@
import React from 'react'
import {FlatList, View} from 'react-native'
import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {
@@ -9,8 +9,9 @@ import {
import {ViewHeader} from '../com/util/ViewHeader'
import {Feed} from '../com/notifications/Feed'
import {TextLink} from 'view/com/util/Link'
import {ListMethods} from 'view/com/util/List'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
import {MainScrollProvider} from '../com/util/MainScrollProvider'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {s, colors} from 'lib/styles'
@@ -35,8 +36,8 @@ type Props = NativeStackScreenProps<
export function NotificationsScreen({}: Props) {
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll()
const scrollElRef = React.useRef<FlatList>(null)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const scrollElRef = React.useRef<ListMethods>(null)
const checkLatestRef = React.useRef<() => void | null>()
const {screen} = useAnalytics()
const pal = usePalette('default')
@@ -50,8 +51,8 @@ export function NotificationsScreen({}: Props) {
// =
const scrollToTop = React.useCallback(() => {
scrollElRef.current?.scrollToOffset({animated: isNative, offset: 0})
resetMainScroll()
}, [scrollElRef, resetMainScroll])
setMinimalShellMode(false)
}, [scrollElRef, setMinimalShellMode])
const onPressLoadLatest = React.useCallback(() => {
scrollToTop()
@@ -130,11 +131,13 @@ export function NotificationsScreen({}: Props) {
return (
<View testID="notificationsScreen" style={s.hContentRegion}>
<ViewHeader title={_(msg`Notifications`)} canGoBack={false} />
<Feed
onScroll={onMainScroll}
scrollElRef={scrollElRef}
ListHeaderComponent={ListHeaderComponent}
/>
<MainScrollProvider>
<Feed
onScrolledDownChange={setIsScrolledDown}
scrollElRef={scrollElRef}
ListHeaderComponent={ListHeaderComponent}
/>
</MainScrollProvider>
{(isScrolledDown || hasNew) && (
<LoadLatestBtn
onPress={onPressLoadLatest}
+3 -1
View File
@@ -24,11 +24,13 @@ import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {ErrorMessage} from '../com/util/error/ErrorMessage'
import {CenteredView} from '../com/util/Views'
import {useComposerControls} from '#/state/shell/composer'
import {useSession} from '#/state/session'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
export function PostThreadScreen({route}: Props) {
const queryClient = useQueryClient()
const {_} = useLingui()
const {hasSession} = useSession()
const {fabMinimalShellTransform} = useMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode()
const {openComposer} = useComposerControls()
@@ -89,7 +91,7 @@ export function PostThreadScreen({route}: Props) {
/>
)}
</View>
{isMobile && canReply && (
{isMobile && canReply && hasSession && (
<Animated.View
style={[
styles.prompt,
+19 -65
View File
@@ -5,7 +5,8 @@ import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {CenteredView, FlatList} from '../com/util/Views'
import {CenteredView} from '../com/util/Views'
import {ListRef} from '../com/util/List'
import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
import {Feed} from 'view/com/posts/Feed'
import {ProfileLists} from '../com/lists/ProfileLists'
@@ -20,7 +21,6 @@ import {useAnalytics} from 'lib/analytics/analytics'
import {ComposeIcon2} from 'lib/icons'
import {useSetTitle} from 'lib/hooks/useSetTitle'
import {combinedDisplayName} from 'lib/strings/display-names'
import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useProfileQuery} from '#/state/queries/profile'
@@ -153,7 +153,7 @@ function ProfileScreenLoaded({
const isMe = profile.did === currentAccount?.did
const showRepliesTab = hasSession
const showLikesTab = isMe
const showFeedsTab = isMe || extraInfoQuery.data?.hasFeedgens
const showFeedsTab = hasSession && (isMe || extraInfoQuery.data?.hasFeedgens)
const showListsTab = hasSession && (isMe || extraInfoQuery.data?.hasLists)
const sectionTitles = useMemo<string[]>(() => {
return [
@@ -277,103 +277,67 @@ function ProfileScreenLoaded({
onPageSelected={onPageSelected}
onCurrentPageSelected={onCurrentPageSelected}
renderHeader={renderHeader}>
{({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => (
{({headerHeight, isFocused, scrollElRef}) => (
<FeedSection
ref={postsSectionRef}
feed={`author|${profile.did}|posts_and_author_threads`}
onScroll={onScroll}
headerHeight={headerHeight}
isFocused={isFocused}
isScrolledDown={isScrolledDown}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
scrollElRef={scrollElRef as ListRef}
ignoreFilterFor={profile.did}
/>
)}
{showRepliesTab
? ({
onScroll,
headerHeight,
isFocused,
isScrolledDown,
scrollElRef,
}) => (
? ({headerHeight, isFocused, scrollElRef}) => (
<FeedSection
ref={repliesSectionRef}
feed={`author|${profile.did}|posts_with_replies`}
onScroll={onScroll}
headerHeight={headerHeight}
isFocused={isFocused}
isScrolledDown={isScrolledDown}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
scrollElRef={scrollElRef as ListRef}
ignoreFilterFor={profile.did}
/>
)
: null}
{({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => (
{({headerHeight, isFocused, scrollElRef}) => (
<FeedSection
ref={mediaSectionRef}
feed={`author|${profile.did}|posts_with_media`}
onScroll={onScroll}
headerHeight={headerHeight}
isFocused={isFocused}
isScrolledDown={isScrolledDown}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
scrollElRef={scrollElRef as ListRef}
ignoreFilterFor={profile.did}
/>
)}
{showLikesTab
? ({
onScroll,
headerHeight,
isFocused,
isScrolledDown,
scrollElRef,
}) => (
? ({headerHeight, isFocused, scrollElRef}) => (
<FeedSection
ref={likesSectionRef}
feed={`likes|${profile.did}`}
onScroll={onScroll}
headerHeight={headerHeight}
isFocused={isFocused}
isScrolledDown={isScrolledDown}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
scrollElRef={scrollElRef as ListRef}
ignoreFilterFor={profile.did}
/>
)
: null}
{showFeedsTab
? ({onScroll, headerHeight, isFocused, scrollElRef}) => (
? ({headerHeight, isFocused, scrollElRef}) => (
<ProfileFeedgens
ref={feedsSectionRef}
did={profile.did}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
onScroll={onScroll}
scrollEventThrottle={1}
scrollElRef={scrollElRef as ListRef}
headerOffset={headerHeight}
enabled={isFocused}
/>
)
: null}
{showListsTab
? ({onScroll, headerHeight, isFocused, scrollElRef}) => (
? ({headerHeight, isFocused, scrollElRef}) => (
<ProfileLists
ref={listsSectionRef}
did={profile.did}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
onScroll={onScroll}
scrollEventThrottle={1}
scrollElRef={scrollElRef as ListRef}
headerOffset={headerHeight}
enabled={isFocused}
/>
@@ -396,28 +360,19 @@ function ProfileScreenLoaded({
interface FeedSectionProps {
feed: FeedDescriptor
onScroll: OnScrollHandler
headerHeight: number
isFocused: boolean
isScrolledDown: boolean
scrollElRef: React.MutableRefObject<FlatList<any> | null>
scrollElRef: ListRef
ignoreFilterFor?: string
}
const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
function FeedSectionImpl(
{
feed,
onScroll,
headerHeight,
isFocused,
isScrolledDown,
scrollElRef,
ignoreFilterFor,
},
{feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor},
ref,
) {
const queryClient = useQueryClient()
const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const onScrollToTop = React.useCallback(() => {
scrollElRef.current?.scrollToOffset({
@@ -443,8 +398,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
feed={feed}
scrollElRef={scrollElRef}
onHasNew={setHasNew}
onScroll={onScroll}
scrollEventThrottle={1}
onScrolledDownChange={setIsScrolledDown}
renderEmptyState={renderPostsEmpty}
headerOffset={headerHeight}
renderEndOfFeed={ProfileEndOfFeed}
+32 -101
View File
@@ -1,25 +1,20 @@
import React, {useMemo, useCallback} from 'react'
import {
Dimensions,
StyleSheet,
View,
ActivityIndicator,
FlatList,
} from 'react-native'
import {Dimensions, StyleSheet, View, ActivityIndicator} from 'react-native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useNavigation} from '@react-navigation/native'
import {useIsFocused, useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {usePalette} from 'lib/hooks/usePalette'
import {HeartIcon, HeartIconSolid} from 'lib/icons'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {CommonNavigatorParams} from 'lib/routes/types'
import {makeRecordUri} from 'lib/strings/url-helpers'
import {colors, s} from 'lib/styles'
import {s} from 'lib/styles'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
import {Feed} from 'view/com/posts/Feed'
import {TextLink} from 'view/com/util/Link'
import {ListRef} from 'view/com/util/List'
import {Button} from 'view/com/util/forms/Button'
import {Text} from 'view/com/util/text/Text'
import {RichText} from 'view/com/util/text/RichText'
@@ -29,12 +24,13 @@ import {EmptyState} from 'view/com/util/EmptyState'
import * as Toast from 'view/com/util/Toast'
import {useSetTitle} from 'lib/hooks/useSetTitle'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {shareUrl} from 'lib/sharing'
import {toShareUrl} from 'lib/strings/url-helpers'
import {Haptics} from 'lib/haptics'
import {useAnalytics} from 'lib/analytics/analytics'
import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {makeCustomFeedLink} from 'lib/routes/links'
import {pluralize} from 'lib/strings/helpers'
import {CenteredView, ScrollView} from 'view/com/util/Views'
@@ -46,12 +42,7 @@ import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {
useFeedSourceInfoQuery,
FeedSourceFeedInfo,
useIsFeedPublicQuery,
} from '#/state/queries/feed'
import {useFeedSourceInfoQuery, FeedSourceFeedInfo} from '#/state/queries/feed'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {
UsePreferencesQueryResponse,
@@ -137,10 +128,8 @@ export function ProfileFeedScreen(props: Props) {
function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
const {data: preferences} = usePreferencesQuery()
const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
const {isLoading: isPublicStatusLoading, data: isPublicResponse} =
useIsFeedPublicQuery({uri: feedUri})
if (!preferences || !info || isPublicStatusLoading) {
if (!preferences || !info) {
return (
<CenteredView>
<View style={s.p20}>
@@ -154,7 +143,6 @@ function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
<ProfileFeedScreenInner
preferences={preferences}
feedInfo={info as FeedSourceFeedInfo}
isPublicResponse={isPublicResponse}
/>
)
}
@@ -162,11 +150,9 @@ function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
export function ProfileFeedScreenInner({
preferences,
feedInfo,
isPublicResponse,
}: {
preferences: UsePreferencesQueryResponse
feedInfo: FeedSourceFeedInfo
isPublicResponse: ReturnType<typeof useIsFeedPublicQuery>['data']
}) {
const {_} = useLingui()
const pal = usePalette('default')
@@ -175,6 +161,7 @@ export function ProfileFeedScreenInner({
const {openComposer} = useComposerControls()
const {track} = useAnalytics()
const feedSectionRef = React.useRef<SectionRef>(null)
const isScreenFocused = useIsFocused()
const {
mutateAsync: saveFeed,
@@ -210,6 +197,9 @@ export function ProfileFeedScreenInner({
useSetTitle(feedInfo?.displayName)
// event handlers
//
const onToggleSaved = React.useCallback(async () => {
try {
Haptics.default()
@@ -403,32 +393,21 @@ export function ProfileFeedScreenInner({
isHeaderReady={true}
renderHeader={renderHeader}
onCurrentPageSelected={onCurrentPageSelected}>
{({onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused}) =>
isPublicResponse?.isPublic ? (
<FeedSection
ref={feedSectionRef}
feed={`feedgen|${feedInfo.uri}`}
onScroll={onScroll}
headerHeight={headerHeight}
isScrolledDown={isScrolledDown}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
isFocused={isFocused}
/>
) : (
<CenteredView sideBorders style={[{paddingTop: headerHeight}]}>
<NonPublicFeedMessage rawError={isPublicResponse?.error} />
</CenteredView>
)
}
{({onScroll, headerHeight, scrollElRef}) => (
{({headerHeight, scrollElRef, isFocused}) => (
<FeedSection
ref={feedSectionRef}
feed={`feedgen|${feedInfo.uri}`}
headerHeight={headerHeight}
scrollElRef={scrollElRef as ListRef}
isFocused={isScreenFocused && isFocused}
/>
)}
{({headerHeight, scrollElRef}) => (
<AboutSection
feedOwnerDid={feedInfo.creatorDid}
feedRkey={feedInfo.route.params.rkey}
feedInfo={feedInfo}
headerHeight={headerHeight}
onScroll={onScroll}
scrollElRef={
scrollElRef as React.MutableRefObject<ScrollView | null>
}
@@ -456,59 +435,16 @@ export function ProfileFeedScreenInner({
)
}
function NonPublicFeedMessage({rawError}: {rawError?: Error}) {
const pal = usePalette('default')
return (
<View
style={[
pal.border,
{
padding: 18,
borderTopWidth: 1,
minHeight: Dimensions.get('window').height * 1.5,
},
]}>
<View
style={[
pal.viewLight,
{
padding: 12,
borderRadius: 8,
gap: 12,
},
]}>
<Text style={[pal.text]}>
<Trans>
Looks like this feed is only available to users with a Bluesky
account. Please sign up or sign in to view this feed!
</Trans>
</Text>
{rawError?.message && (
<Text style={pal.textLight}>
<Trans>Message from server</Trans>: {rawError.message}
</Text>
)}
</View>
</View>
)
}
interface FeedSectionProps {
feed: FeedDescriptor
onScroll: OnScrollHandler
headerHeight: number
isScrolledDown: boolean
scrollElRef: React.MutableRefObject<FlatList<any> | null>
scrollElRef: ListRef
isFocused: boolean
}
const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
function FeedSectionImpl(
{feed, onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused},
ref,
) {
function FeedSectionImpl({feed, headerHeight, scrollElRef, isFocused}, ref) {
const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const queryClient = useQueryClient()
const onScrollToTop = useCallback(() => {
@@ -533,11 +469,10 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
<Feed
enabled={isFocused}
feed={feed}
pollInterval={30e3}
pollInterval={60e3}
scrollElRef={scrollElRef}
onHasNew={setHasNew}
onScroll={onScroll}
scrollEventThrottle={5}
onScrolledDownChange={setIsScrolledDown}
renderEmptyState={renderPostsEmpty}
headerOffset={headerHeight}
/>
@@ -558,7 +493,6 @@ function AboutSection({
feedRkey,
feedInfo,
headerHeight,
onScroll,
scrollElRef,
isOwner,
}: {
@@ -566,13 +500,13 @@ function AboutSection({
feedRkey: string
feedInfo: FeedSourceFeedInfo
headerHeight: number
onScroll: OnScrollHandler
scrollElRef: React.MutableRefObject<ScrollView | null>
isOwner: boolean
}) {
const pal = usePalette('default')
const {_} = useLingui()
const scrollHandler = useAnimatedScrollHandler(onScroll)
const scrollHandlers = useScrollHandlers()
const onScroll = useAnimatedScrollHandler(scrollHandlers)
const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri)
const {hasSession} = useSession()
const {track} = useAnalytics()
@@ -608,12 +542,12 @@ function AboutSection({
return (
<ScrollView
ref={scrollElRef}
onScroll={onScroll}
scrollEventThrottle={1}
contentContainerStyle={{
paddingTop: headerHeight,
minHeight: Dimensions.get('window').height * 1.5,
}}
onScroll={scrollHandler}>
}}>
<View
style={[
{
@@ -646,7 +580,7 @@ function AboutSection({
onPress={onToggleLiked}
style={{paddingHorizontal: 10}}>
{isLiked ? (
<HeartIconSolid size={19} style={styles.liked} />
<HeartIconSolid size={19} style={s.likeColor} />
) : (
<HeartIcon strokeWidth={3} size={19} style={pal.textLight} />
)}
@@ -689,9 +623,6 @@ const styles = StyleSheet.create({
borderRadius: 50,
marginLeft: 6,
},
liked: {
color: colors.red3,
},
notFoundContainer: {
margin: 10,
paddingHorizontal: 18,
+20 -50
View File
@@ -1,12 +1,6 @@
import React, {useCallback, useMemo} from 'react'
import {
ActivityIndicator,
FlatList,
Pressable,
StyleSheet,
View,
} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native'
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {useNavigation} from '@react-navigation/native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@@ -22,6 +16,7 @@ import {EmptyState} from 'view/com/util/EmptyState'
import {RichText} from 'view/com/util/text/RichText'
import {Button} from 'view/com/util/forms/Button'
import {TextLink} from 'view/com/util/Link'
import {ListRef} from 'view/com/util/List'
import * as Toast from 'view/com/util/Toast'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {FAB} from 'view/com/util/fab/FAB'
@@ -31,7 +26,6 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useSetTitle} from 'lib/hooks/useSetTitle'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {NavigationProp} from 'lib/routes/types'
import {toShareUrl} from 'lib/strings/url-helpers'
import {shareUrl} from 'lib/sharing'
@@ -121,6 +115,7 @@ function ProfileListScreenLoaded({
const aboutSectionRef = React.useRef<SectionRef>(null)
const {openModal} = useModalControls()
const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist'
const isScreenFocused = useIsFocused()
useSetTitle(list.name)
@@ -165,36 +160,22 @@ function ProfileListScreenLoaded({
isHeaderReady={true}
renderHeader={renderHeader}
onCurrentPageSelected={onCurrentPageSelected}>
{({
onScroll,
headerHeight,
isScrolledDown,
scrollElRef,
isFocused,
}) => (
{({headerHeight, scrollElRef, isFocused}) => (
<FeedSection
ref={feedSectionRef}
feed={`list|${uri}`}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
onScroll={onScroll}
scrollElRef={scrollElRef as ListRef}
headerHeight={headerHeight}
isScrolledDown={isScrolledDown}
isFocused={isFocused}
isFocused={isScreenFocused && isFocused}
/>
)}
{({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
{({headerHeight, scrollElRef}) => (
<AboutSection
ref={aboutSectionRef}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
scrollElRef={scrollElRef as ListRef}
list={list}
onPressAddUser={onPressAddUser}
onScroll={onScroll}
headerHeight={headerHeight}
isScrolledDown={isScrolledDown}
/>
)}
</PagerWithHeader>
@@ -221,16 +202,12 @@ function ProfileListScreenLoaded({
items={SECTION_TITLES_MOD}
isHeaderReady={true}
renderHeader={renderHeader}>
{({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
{({headerHeight, scrollElRef}) => (
<AboutSection
list={list}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
scrollElRef={scrollElRef as ListRef}
onPressAddUser={onPressAddUser}
onScroll={onScroll}
headerHeight={headerHeight}
isScrolledDown={isScrolledDown}
/>
)}
</PagerWithHeader>
@@ -615,19 +592,15 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
interface FeedSectionProps {
feed: FeedDescriptor
onScroll: OnScrollHandler
headerHeight: number
isScrolledDown: boolean
scrollElRef: React.MutableRefObject<FlatList<any> | null>
scrollElRef: ListRef
isFocused: boolean
}
const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
function FeedSectionImpl(
{feed, scrollElRef, onScroll, headerHeight, isScrolledDown, isFocused},
ref,
) {
function FeedSectionImpl({feed, scrollElRef, headerHeight, isFocused}, ref) {
const queryClient = useQueryClient()
const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({
@@ -651,11 +624,10 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
testID="listFeed"
enabled={isFocused}
feed={feed}
pollInterval={30e3}
pollInterval={60e3}
scrollElRef={scrollElRef}
onHasNew={setHasNew}
onScroll={onScroll}
scrollEventThrottle={1}
onScrolledDownChange={setIsScrolledDown}
renderEmptyState={renderPostsEmpty}
headerOffset={headerHeight}
/>
@@ -674,20 +646,19 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
interface AboutSectionProps {
list: AppBskyGraphDefs.ListView
onPressAddUser: () => void
onScroll: OnScrollHandler
headerHeight: number
isScrolledDown: boolean
scrollElRef: React.MutableRefObject<FlatList<any> | null>
scrollElRef: ListRef
}
const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
function AboutSectionImpl(
{list, onPressAddUser, onScroll, headerHeight, isScrolledDown, scrollElRef},
{list, onPressAddUser, headerHeight, scrollElRef},
ref,
) {
const pal = usePalette('default')
const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
const {currentAccount} = useSession()
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist'
const isOwner = list.creator.did === currentAccount?.did
@@ -817,8 +788,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
renderHeader={renderHeader}
renderEmptyState={renderEmptyState}
headerOffset={headerHeight}
onScroll={onScroll}
scrollEventThrottle={1}
onScrolledDownChange={setIsScrolledDown}
/>
{isScrolledDown && (
<LoadLatestBtn
+71 -37
View File
@@ -8,7 +8,8 @@ import {
Pressable,
Platform,
} from 'react-native'
import {FlatList, ScrollView, CenteredView} from '#/view/com/util/Views'
import {ScrollView, CenteredView} from '#/view/com/util/Views'
import {List} from '#/view/com/util/List'
import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -155,7 +156,7 @@ function SearchScreenSuggestedFollows() {
}, [currentAccount, setSuggestions, getSuggestedFollowsByActor])
return suggestions.length ? (
<FlatList
<List
data={suggestions}
renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} noBg />}
keyExtractor={item => item.did}
@@ -243,7 +244,7 @@ function SearchScreenPostResults({query}: {query: string}) {
{isFetched ? (
<>
{posts.length ? (
<FlatList
<List
data={items}
renderItem={({item}) => {
if (item.type === 'post') {
@@ -284,7 +285,7 @@ function SearchScreenUserResults({query}: {query: string}) {
return isFetched && results ? (
<>
{results.length ? (
<FlatList
<List
data={results}
renderItem={({item}) => (
<ProfileCardWithFollowBtn profile={item} noBg />
@@ -303,7 +304,8 @@ function SearchScreenUserResults({query}: {query: string}) {
)
}
const SECTIONS = ['Posts', 'Users']
const SECTIONS_LOGGEDOUT = ['Users']
const SECTIONS_LOGGEDIN = ['Posts', 'Users']
export function SearchScreenInner({query}: {query?: string}) {
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
@@ -319,44 +321,62 @@ export function SearchScreenInner({query}: {query?: string}) {
[setDrawerSwipeDisabled, setMinimalShellMode],
)
if (hasSession) {
return query ? (
<Pager
tabBarPosition="top"
onPageSelected={onPageSelected}
renderTabBar={props => (
<CenteredView sideBorders style={pal.border}>
<TabBar items={SECTIONS_LOGGEDIN} {...props} />
</CenteredView>
)}
initialPage={0}>
<View>
<SearchScreenPostResults query={query} />
</View>
<View>
<SearchScreenUserResults query={query} />
</View>
</Pager>
) : (
<View>
<CenteredView sideBorders style={pal.border}>
<Text
type="title"
style={[
pal.text,
pal.border,
{
display: 'flex',
paddingVertical: 12,
paddingHorizontal: 18,
fontWeight: 'bold',
},
]}>
<Trans>Suggested Follows</Trans>
</Text>
</CenteredView>
<SearchScreenSuggestedFollows />
</View>
)
}
return query ? (
<Pager
tabBarPosition="top"
onPageSelected={onPageSelected}
renderTabBar={props => (
<CenteredView sideBorders style={pal.border}>
<TabBar items={SECTIONS} {...props} />
<TabBar items={SECTIONS_LOGGEDOUT} {...props} />
</CenteredView>
)}
initialPage={0}>
<View>
<SearchScreenPostResults query={query} />
</View>
<View>
<SearchScreenUserResults query={query} />
</View>
</Pager>
) : hasSession ? (
<View>
<CenteredView sideBorders style={pal.border}>
<Text
type="title"
style={[
pal.text,
pal.border,
{
display: 'flex',
paddingVertical: 12,
paddingHorizontal: 18,
fontWeight: 'bold',
},
]}>
<Trans>Suggested Follows</Trans>
</Text>
</CenteredView>
<SearchScreenSuggestedFollows />
</View>
) : (
<CenteredView sideBorders style={pal.border}>
<View
@@ -382,13 +402,27 @@ export function SearchScreenInner({query}: {query?: string}) {
</Text>
)}
<Text
style={[
pal.textLight,
{textAlign: 'center', paddingVertical: 12, paddingHorizontal: 18},
]}>
<Trans>Search for posts and users.</Trans>
</Text>
<View
style={{
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 30,
gap: 15,
}}>
<MagnifyingGlassIcon
strokeWidth={3}
size={isDesktop ? 60 : 60}
style={pal.textLight}
/>
<Text type="xl" style={[pal.textLight, {paddingHorizontal: 18}]}>
{isDesktop ? (
<Trans>Find users with the search tool on the right</Trans>
) : (
<Trans>Find users on Bluesky</Trans>
)}
</Text>
</View>
</View>
</CenteredView>
)
+12 -12
View File
@@ -221,19 +221,17 @@ let DrawerContent = ({}: {}): React.ReactNode => {
<NavSignupCard />
)}
{hasSession && <InviteCodes />}
{hasSession && <View style={{height: 10}} />}
<SearchMenuItem isActive={isAtSearch} onPress={onPressSearch} />
<HomeMenuItem isActive={isAtHome} onPress={onPressHome} />
{hasSession && (
<NotificationsMenuItem
isActive={isAtNotifications}
onPress={onPressNotifications}
/>
)}
<FeedsMenuItem isActive={isAtFeeds} onPress={onPressMyFeeds} />
{hasSession && (
{hasSession ? (
<>
<InviteCodes />
<View style={{height: 10}} />
<SearchMenuItem isActive={isAtSearch} onPress={onPressSearch} />
<HomeMenuItem isActive={isAtHome} onPress={onPressHome} />
<NotificationsMenuItem
isActive={isAtNotifications}
onPress={onPressNotifications}
/>
<FeedsMenuItem isActive={isAtFeeds} onPress={onPressMyFeeds} />
<ListsMenuItem onPress={onPressLists} />
<ModerationMenuItem onPress={onPressModeration} />
<ProfileMenuItem
@@ -242,6 +240,8 @@ let DrawerContent = ({}: {}): React.ReactNode => {
/>
<SettingsMenuItem onPress={onPressSettings} />
</>
) : (
<SearchMenuItem isActive={isAtSearch} onPress={onPressSearch} />
)}
<View style={styles.smallSpacer} />
+16 -10
View File
@@ -5,22 +5,28 @@ import {useLingui} from '@lingui/react'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {DefaultAvatar} from '#/view/com/util/UserAvatar'
import {Text} from '#/view/com/util/text/Text'
import {Button} from '#/view/com/util/forms/Button'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import {Logo} from '#/view/icons/Logo'
let NavSignupCard = ({}: {}): React.ReactNode => {
const {_} = useLingui()
const pal = usePalette('default')
const {setShowLoggedOut} = useLoggedOutViewControls()
const {requestSwitchToAccount} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements()
const showLoggedOut = React.useCallback(() => {
const showSignIn = React.useCallback(() => {
closeAllActiveElements()
setShowLoggedOut(true)
}, [setShowLoggedOut, closeAllActiveElements])
requestSwitchToAccount({requestedAccount: 'none'})
}, [requestSwitchToAccount, closeAllActiveElements])
const showCreateAccount = React.useCallback(() => {
closeAllActiveElements()
requestSwitchToAccount({requestedAccount: 'new'})
// setShowLoggedOut(true)
}, [requestSwitchToAccount, closeAllActiveElements])
return (
<View
@@ -29,17 +35,17 @@ let NavSignupCard = ({}: {}): React.ReactNode => {
paddingTop: 6,
marginBottom: 24,
}}>
<DefaultAvatar type="user" size={48} />
<Logo width={48} />
<View style={{paddingTop: 12}}>
<Text type="md" style={[pal.text, s.bold]}>
<View style={{paddingTop: 18}}>
<Text type="md-bold" style={[pal.text]}>
<Trans>Sign up or sign in to join the conversation</Trans>
</Text>
</View>
<View style={{flexDirection: 'row', paddingTop: 12, gap: 8}}>
<Button
onPress={showLoggedOut}
onPress={showCreateAccount}
accessibilityHint={_(msg`Sign up`)}
accessibilityLabel={_(msg`Sign up`)}>
<Text type="md" style={[{color: 'white'}, s.bold]}>
@@ -48,7 +54,7 @@ let NavSignupCard = ({}: {}): React.ReactNode => {
</Button>
<Button
type="default"
onPress={showLoggedOut}
onPress={showSignIn}
accessibilityHint={_(msg`Sign in`)}
accessibilityLabel={_(msg`Sign in`)}>
<Text type="md" style={[pal.text, s.bold]}>
+130 -69
View File
@@ -23,13 +23,19 @@ import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import {useNavigationTabState} from 'lib/hooks/useNavigationTabState'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
import {useShellLayout} from '#/state/shell/shell-layout'
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
import {emitSoftReset} from '#/state/events'
import {useSession} from '#/state/session'
import {useProfileQuery} from '#/state/queries/profile'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import {Button} from '#/view/com/util/forms/Button'
import {s} from 'lib/styles'
import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype'
type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds'
@@ -46,6 +52,19 @@ export function BottomBar({navigation}: BottomTabBarProps) {
const numUnreadNotifications = useUnreadNotifications()
const {footerMinimalShellTransform} = useMinimalShellMode()
const {data: profile} = useProfileQuery({did: currentAccount?.did})
const {requestSwitchToAccount} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements()
const showSignIn = React.useCallback(() => {
closeAllActiveElements()
requestSwitchToAccount({requestedAccount: 'none'})
}, [requestSwitchToAccount, closeAllActiveElements])
const showCreateAccount = React.useCallback(() => {
closeAllActiveElements()
requestSwitchToAccount({requestedAccount: 'new'})
// setShowLoggedOut(true)
}, [requestSwitchToAccount, closeAllActiveElements])
const onPressTab = React.useCallback(
(tab: TabOptions) => {
@@ -94,75 +113,74 @@ export function BottomBar({navigation}: BottomTabBarProps) {
onLayout={e => {
footerHeight.value = e.nativeEvent.layout.height
}}>
<Btn
testID="bottomBarHomeBtn"
icon={
isAtHome ? (
<HomeIconSolid
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
) : (
<HomeIcon
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
)
}
onPress={onPressHome}
accessibilityRole="tab"
accessibilityLabel={_(msg`Home`)}
accessibilityHint=""
/>
<Btn
testID="bottomBarSearchBtn"
icon={
isAtSearch ? (
<MagnifyingGlassIcon2Solid
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
) : (
<MagnifyingGlassIcon2
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
)
}
onPress={onPressSearch}
accessibilityRole="search"
accessibilityLabel={_(msg`Search`)}
accessibilityHint=""
/>
<Btn
testID="bottomBarFeedsBtn"
icon={
isAtFeeds ? (
<HashtagIcon
size={24}
style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
strokeWidth={4}
/>
) : (
<HashtagIcon
size={24}
style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
strokeWidth={2.25}
/>
)
}
onPress={onPressFeeds}
accessibilityRole="tab"
accessibilityLabel={_(msg`Feeds`)}
accessibilityHint=""
/>
{hasSession && (
{hasSession ? (
<>
<Btn
testID="bottomBarHomeBtn"
icon={
isAtHome ? (
<HomeIconSolid
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
) : (
<HomeIcon
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
)
}
onPress={onPressHome}
accessibilityRole="tab"
accessibilityLabel={_(msg`Home`)}
accessibilityHint=""
/>
<Btn
testID="bottomBarSearchBtn"
icon={
isAtSearch ? (
<MagnifyingGlassIcon2Solid
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
) : (
<MagnifyingGlassIcon2
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
)
}
onPress={onPressSearch}
accessibilityRole="search"
accessibilityLabel={_(msg`Search`)}
accessibilityHint=""
/>
<Btn
testID="bottomBarFeedsBtn"
icon={
isAtFeeds ? (
<HashtagIcon
size={24}
style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
strokeWidth={4}
/>
) : (
<HashtagIcon
size={24}
style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
strokeWidth={2.25}
/>
)
}
onPress={onPressFeeds}
accessibilityRole="tab"
accessibilityLabel={_(msg`Feeds`)}
accessibilityHint=""
/>
<Btn
testID="bottomBarNotificationsBtn"
icon={
@@ -230,6 +248,49 @@ export function BottomBar({navigation}: BottomTabBarProps) {
accessibilityHint=""
/>
</>
) : (
<>
<View
style={{
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: 14,
paddingBottom: 2,
paddingLeft: 14,
paddingRight: 6,
gap: 8,
}}>
<View style={{flexDirection: 'row', alignItems: 'center', gap: 8}}>
<Logo width={28} />
<View style={{paddingTop: 4}}>
<Logotype width={80} fill={pal.text.color} />
</View>
</View>
<View style={{flexDirection: 'row', alignItems: 'center', gap: 4}}>
<Button
onPress={showCreateAccount}
accessibilityHint={_(msg`Sign up`)}
accessibilityLabel={_(msg`Sign up`)}>
<Text type="md" style={[{color: 'white'}, s.bold]}>
<Trans>Sign up</Trans>
</Text>
</Button>
<Button
type="default"
onPress={showSignIn}
accessibilityHint={_(msg`Sign in`)}
accessibilityLabel={_(msg`Sign in`)}>
<Text type="md" style={[pal.text, s.bold]}>
<Trans>Sign in</Trans>
</Text>
</Button>
</View>
</View>
</>
)}
</Animated.View>
)
+128 -57
View File
@@ -3,6 +3,9 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useNavigationState} from '@react-navigation/native'
import Animated from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {getCurrentRoute, isTab} from 'lib/routes/helpers'
import {styles} from './BottomBarStyles'
import {clamp} from 'lib/numbers'
@@ -22,12 +25,33 @@ import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import {makeProfileLink} from 'lib/routes/links'
import {CommonNavigatorParams} from 'lib/routes/types'
import {useSession} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import {Button} from '#/view/com/util/forms/Button'
import {Text} from '#/view/com/util/text/Text'
import {s} from 'lib/styles'
import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype'
export function BottomBarWeb() {
const {_} = useLingui()
const {hasSession, currentAccount} = useSession()
const pal = usePalette('default')
const safeAreaInsets = useSafeAreaInsets()
const {footerMinimalShellTransform} = useMinimalShellMode()
const {requestSwitchToAccount} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements()
const showSignIn = React.useCallback(() => {
closeAllActiveElements()
requestSwitchToAccount({requestedAccount: 'none'})
}, [requestSwitchToAccount, closeAllActiveElements])
const showCreateAccount = React.useCallback(() => {
closeAllActiveElements()
requestSwitchToAccount({requestedAccount: 'new'})
// setShowLoggedOut(true)
}, [requestSwitchToAccount, closeAllActiveElements])
return (
<Animated.View
@@ -38,79 +62,126 @@ export function BottomBarWeb() {
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
footerMinimalShellTransform,
]}>
<NavItem routeName="Home" href="/">
{({isActive}) => {
const Icon = isActive ? HomeIconSolid : HomeIcon
return (
<Icon
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
)
}}
</NavItem>
<NavItem routeName="Search" href="/search">
{({isActive}) => {
const Icon = isActive
? MagnifyingGlassIcon2Solid
: MagnifyingGlassIcon2
return (
<Icon
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
)
}}
</NavItem>
<NavItem routeName="Feeds" href="/feeds">
{({isActive}) => {
return (
<HashtagIcon
size={22}
style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
strokeWidth={isActive ? 4 : 2.5}
/>
)
}}
</NavItem>
{hasSession && (
{hasSession ? (
<>
<NavItem routeName="Notifications" href="/notifications">
<NavItem routeName="Home" href="/">
{({isActive}) => {
const Icon = isActive ? BellIconSolid : BellIcon
const Icon = isActive ? HomeIconSolid : HomeIcon
return (
<Icon
strokeWidth={4}
size={24}
strokeWidth={1.9}
style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
)
}}
</NavItem>
<NavItem
routeName="Profile"
href={
currentAccount
? makeProfileLink({
did: currentAccount.did,
handle: currentAccount.handle,
})
: '/'
}>
<NavItem routeName="Search" href="/search">
{({isActive}) => {
const Icon = isActive ? UserIconSolid : UserIcon
const Icon = isActive
? MagnifyingGlassIcon2Solid
: MagnifyingGlassIcon2
return (
<Icon
size={28}
strokeWidth={1.5}
style={[styles.ctrlIcon, pal.text, styles.profileIcon]}
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
)
}}
</NavItem>
{hasSession && (
<>
<NavItem routeName="Feeds" href="/feeds">
{({isActive}) => {
return (
<HashtagIcon
size={22}
style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
strokeWidth={isActive ? 4 : 2.5}
/>
)
}}
</NavItem>
<NavItem routeName="Notifications" href="/notifications">
{({isActive}) => {
const Icon = isActive ? BellIconSolid : BellIcon
return (
<Icon
size={24}
strokeWidth={1.9}
style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
/>
)
}}
</NavItem>
<NavItem
routeName="Profile"
href={
currentAccount
? makeProfileLink({
did: currentAccount.did,
handle: currentAccount.handle,
})
: '/'
}>
{({isActive}) => {
const Icon = isActive ? UserIconSolid : UserIcon
return (
<Icon
size={28}
strokeWidth={1.5}
style={[styles.ctrlIcon, pal.text, styles.profileIcon]}
/>
)
}}
</NavItem>
</>
)}
</>
) : (
<>
<View
style={{
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: 14,
paddingBottom: 2,
paddingLeft: 14,
paddingRight: 6,
gap: 8,
}}>
<View style={{flexDirection: 'row', alignItems: 'center', gap: 12}}>
<Logo width={32} />
<View style={{paddingTop: 4}}>
<Logotype width={80} fill={pal.text.color} />
</View>
</View>
<View style={{flexDirection: 'row', alignItems: 'center', gap: 8}}>
<Button
onPress={showCreateAccount}
accessibilityHint={_(msg`Sign up`)}
accessibilityLabel={_(msg`Sign up`)}>
<Text type="md" style={[{color: 'white'}, s.bold]}>
<Trans>Sign up</Trans>
</Text>
</Button>
<Button
type="default"
onPress={showSignIn}
accessibilityHint={_(msg`Sign in`)}
accessibilityLabel={_(msg`Sign in`)}>
<Text type="md" style={[pal.text, s.bold]}>
<Trans>Sign in</Trans>
</Text>
</Button>
</View>
</View>
</>
)}
</Animated.View>
+55 -53
View File
@@ -266,6 +266,10 @@ export function DesktopLeftNav() {
const {isDesktop, isTablet} = useWebMediaQueries()
const numUnread = useUnreadNotifications()
if (!hasSession && !isDesktop) {
return null
}
return (
<View
style={[
@@ -282,59 +286,58 @@ export function DesktopLeftNav() {
</View>
) : null}
<BackBtn />
<NavItem
href="/"
icon={<HomeIcon size={isDesktop ? 24 : 28} style={pal.text} />}
iconFilled={
<HomeIconSolid
strokeWidth={4}
size={isDesktop ? 24 : 28}
style={pal.text}
/>
}
label={_(msg`Home`)}
/>
<NavItem
href="/search"
icon={
<MagnifyingGlassIcon2
strokeWidth={2}
size={isDesktop ? 24 : 26}
style={pal.text}
/>
}
iconFilled={
<MagnifyingGlassIcon2Solid
strokeWidth={2}
size={isDesktop ? 24 : 26}
style={pal.text}
/>
}
label={_(msg`Search`)}
/>
<NavItem
href="/feeds"
icon={
<HashtagIcon
strokeWidth={2.25}
style={pal.text as FontAwesomeIconStyle}
size={isDesktop ? 24 : 28}
/>
}
iconFilled={
<HashtagIcon
strokeWidth={2.5}
style={pal.text as FontAwesomeIconStyle}
size={isDesktop ? 24 : 28}
/>
}
label={_(msg`Feeds`)}
/>
{hasSession && (
<>
<BackBtn />
<NavItem
href="/"
icon={<HomeIcon size={isDesktop ? 24 : 28} style={pal.text} />}
iconFilled={
<HomeIconSolid
strokeWidth={4}
size={isDesktop ? 24 : 28}
style={pal.text}
/>
}
label={_(msg`Home`)}
/>
<NavItem
href="/search"
icon={
<MagnifyingGlassIcon2
strokeWidth={2}
size={isDesktop ? 24 : 26}
style={pal.text}
/>
}
iconFilled={
<MagnifyingGlassIcon2Solid
strokeWidth={2}
size={isDesktop ? 24 : 26}
style={pal.text}
/>
}
label={_(msg`Search`)}
/>
<NavItem
href="/feeds"
icon={
<HashtagIcon
strokeWidth={2.25}
style={pal.text as FontAwesomeIconStyle}
size={isDesktop ? 24 : 28}
/>
}
iconFilled={
<HashtagIcon
strokeWidth={2.5}
style={pal.text as FontAwesomeIconStyle}
size={isDesktop ? 24 : 28}
/>
}
label={_(msg`Feeds`)}
/>
<NavItem
href="/notifications"
count={numUnread}
@@ -406,7 +409,7 @@ export function DesktopLeftNav() {
style={pal.text}
/>
}
label="Profile"
label={_(msg`Profile`)}
/>
<NavItem
href="/settings"
@@ -512,7 +515,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: 140,
borderRadius: 24,
paddingTop: 10,
paddingBottom: 12, // visually aligns the text vertically inside the button
+1 -1
View File
@@ -52,7 +52,7 @@ export function DesktopRightNav() {
</Text>
</View>
) : undefined}
<View style={[s.flexRow]}>
<View style={[{flexWrap: 'wrap'}, s.flexRow]}>
{hasSession && (
<>
<TextLink
+6 -12
View File
@@ -19,10 +19,6 @@ import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
import {RoutesContainer, TabsNavigator} from '../../Navigation'
import {isStateAtTabRoot} from 'lib/routes/helpers'
import {
SafeAreaProvider,
initialWindowMetrics,
} from 'react-native-safe-area-context'
import {
useIsDrawerOpen,
useSetDrawerOpen,
@@ -107,14 +103,12 @@ export const Shell: React.FC = function ShellImpl() {
const pal = usePalette('default')
const theme = useTheme()
return (
<SafeAreaProvider initialMetrics={initialWindowMetrics} style={pal.view}>
<View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
<StatusBar style={theme.colorScheme === 'dark' ? 'light' : 'dark'} />
<RoutesContainer>
<ShellInner />
</RoutesContainer>
</View>
</SafeAreaProvider>
<View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
<StatusBar style={theme.colorScheme === 'dark' ? 'light' : 'dark'} />
<RoutesContainer>
<ShellInner />
</RoutesContainer>
</View>
)
}