New empty state for feeds
This commit is contained in:
@@ -7,24 +7,33 @@ import {
|
|||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useNavigation} from '@react-navigation/native'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack'
|
||||||
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
import {parseStarterPackUri} from '#/lib/strings/starter-pack'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isNative, isWeb} from '#/platform/detection'
|
import {isNative, isWeb} from '#/platform/detection'
|
||||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
|
import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
|
||||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
|
||||||
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {atoms as a, ios, useTheme} from '#/alf'
|
import {atoms as a, ios, useTheme} from '#/alf'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import * as FeedCard from '#/components/FeedCard'
|
import * as FeedCard from '#/components/FeedCard'
|
||||||
|
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
|
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
|
||||||
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {List, ListRef} from '../util/List'
|
import {List, ListRef} from '../util/List'
|
||||||
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||||
|
|
||||||
const LOADING = {_reactKey: '__loading__'}
|
const LOADING = {_reactKey: '__loading__'}
|
||||||
const EMPTY = {_reactKey: '__empty__'}
|
|
||||||
const ERROR_ITEM = {_reactKey: '__error__'}
|
const ERROR_ITEM = {_reactKey: '__error__'}
|
||||||
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
||||||
|
|
||||||
@@ -40,13 +49,23 @@ interface ProfileFeedgensProps {
|
|||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
testID?: string
|
testID?: string
|
||||||
setScrollViewTag: (tag: number | null) => void
|
setScrollViewTag: (tag: number | null) => void
|
||||||
|
isMe: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProfileFeedgens = React.forwardRef<
|
export const ProfileFeedgens = React.forwardRef<
|
||||||
SectionRef,
|
SectionRef,
|
||||||
ProfileFeedgensProps
|
ProfileFeedgensProps
|
||||||
>(function ProfileFeedgensImpl(
|
>(function ProfileFeedgensImpl(
|
||||||
{did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag},
|
{
|
||||||
|
did,
|
||||||
|
scrollElRef,
|
||||||
|
headerOffset,
|
||||||
|
enabled,
|
||||||
|
style,
|
||||||
|
testID,
|
||||||
|
setScrollViewTag,
|
||||||
|
isMe,
|
||||||
|
},
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
@@ -74,7 +93,7 @@ export const ProfileFeedgens = React.forwardRef<
|
|||||||
if (!isFetched && isFetching) {
|
if (!isFetched && isFetching) {
|
||||||
items = items.concat([LOADING])
|
items = items.concat([LOADING])
|
||||||
} else if (isEmpty) {
|
} else if (isEmpty) {
|
||||||
items = items.concat([EMPTY])
|
// -- handled separately --
|
||||||
} else if (data?.pages) {
|
} else if (data?.pages) {
|
||||||
for (const page of data?.pages) {
|
for (const page of data?.pages) {
|
||||||
items = items.concat(page.feeds)
|
items = items.concat(page.feeds)
|
||||||
@@ -131,15 +150,7 @@ export const ProfileFeedgens = React.forwardRef<
|
|||||||
|
|
||||||
const renderItem = React.useCallback(
|
const renderItem = React.useCallback(
|
||||||
({item, index}: ListRenderItemInfo<any>) => {
|
({item, index}: ListRenderItemInfo<any>) => {
|
||||||
if (item === EMPTY) {
|
if (item === ERROR_ITEM) {
|
||||||
return (
|
|
||||||
<EmptyState
|
|
||||||
icon="hashtag"
|
|
||||||
message={_(msg`You have no feeds.`)}
|
|
||||||
testID="listsEmpty"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} else if (item === ERROR_ITEM) {
|
|
||||||
return (
|
return (
|
||||||
<ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} />
|
<ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} />
|
||||||
)
|
)
|
||||||
@@ -198,7 +209,171 @@ export const ProfileFeedgens = React.forwardRef<
|
|||||||
// @ts-ignore our .web version only -prf
|
// @ts-ignore our .web version only -prf
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
|
ListEmptyComponent={Empty}
|
||||||
|
ListFooterComponent={
|
||||||
|
items?.length !== 0 && isMe ? CreateAnother : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function CreateAnother() {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.pr_md,
|
||||||
|
a.pt_lg,
|
||||||
|
a.gap_lg,
|
||||||
|
a.border_t,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Create a starter pack`)}
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
style={[a.self_center]}
|
||||||
|
onPress={() => navigation.navigate('StarterPackWizard')}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Create another</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
<ButtonIcon icon={Plus} position="right" />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Empty() {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
const confirmDialogControl = useDialogControl()
|
||||||
|
const followersDialogControl = useDialogControl()
|
||||||
|
const errorDialogControl = useDialogControl()
|
||||||
|
|
||||||
|
const [isGenerating, setIsGenerating] = React.useState(false)
|
||||||
|
|
||||||
|
const {mutate: generateStarterPack} = useGenerateStarterPackMutation({
|
||||||
|
onSuccess: ({uri}) => {
|
||||||
|
const parsed = parseStarterPackUri(uri)
|
||||||
|
if (parsed) {
|
||||||
|
navigation.push('StarterPack', {
|
||||||
|
name: parsed.name,
|
||||||
|
rkey: parsed.rkey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setIsGenerating(false)
|
||||||
|
},
|
||||||
|
onError: e => {
|
||||||
|
logger.error('Failed to generate starter pack', {safeMessage: e})
|
||||||
|
setIsGenerating(false)
|
||||||
|
if (e.name === 'NOT_ENOUGH_FOLLOWERS') {
|
||||||
|
followersDialogControl.open()
|
||||||
|
} else {
|
||||||
|
errorDialogControl.open()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const generate = () => {
|
||||||
|
setIsGenerating(true)
|
||||||
|
generateStarterPack()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LinearGradientBackground
|
||||||
|
style={[
|
||||||
|
a.px_lg,
|
||||||
|
a.py_lg,
|
||||||
|
a.justify_between,
|
||||||
|
a.gap_lg,
|
||||||
|
a.shadow_lg,
|
||||||
|
{marginTop: 2},
|
||||||
|
]}>
|
||||||
|
<View style={[a.gap_xs]}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.font_bold,
|
||||||
|
a.text_lg,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
{color: 'white'},
|
||||||
|
]}>
|
||||||
|
<Trans>You haven't created a feed yet!</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.text_md, {color: 'white'}]}>
|
||||||
|
<Trans>
|
||||||
|
Create a feed to curate your own and others people's posts.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[a.flex_row, a.gap_md, {marginLeft: 'auto'}]}>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Create a starter pack`)}
|
||||||
|
variant="ghost"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
disabled={isGenerating}
|
||||||
|
onPress={() => navigation.navigate('StarterPackWizard')}
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderColor: 'white',
|
||||||
|
width: 100,
|
||||||
|
}}
|
||||||
|
hoverStyle={[{backgroundColor: '#dfdfdf'}]}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Create</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Prompt.Outer control={confirmDialogControl}>
|
||||||
|
<Prompt.TitleText>
|
||||||
|
<Trans>Generate a starter pack</Trans>
|
||||||
|
</Prompt.TitleText>
|
||||||
|
<Prompt.DescriptionText>
|
||||||
|
<Trans>
|
||||||
|
Bluesky will choose a set of recommended accounts from people in
|
||||||
|
your network.
|
||||||
|
</Trans>
|
||||||
|
</Prompt.DescriptionText>
|
||||||
|
<Prompt.Actions>
|
||||||
|
<Prompt.Action
|
||||||
|
color="primary"
|
||||||
|
cta={_(msg`Choose for me`)}
|
||||||
|
onPress={generate}
|
||||||
|
/>
|
||||||
|
<Prompt.Action
|
||||||
|
color="secondary"
|
||||||
|
cta={_(msg`Let me choose`)}
|
||||||
|
onPress={() => {
|
||||||
|
navigation.navigate('StarterPackWizard')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Prompt.Actions>
|
||||||
|
</Prompt.Outer>
|
||||||
|
<Prompt.Basic
|
||||||
|
control={followersDialogControl}
|
||||||
|
title={_(msg`Oops!`)}
|
||||||
|
description={_(
|
||||||
|
msg`You must be following at least seven other people to generate a starter pack.`,
|
||||||
|
)}
|
||||||
|
onConfirm={() => {}}
|
||||||
|
showCancel={false}
|
||||||
|
/>
|
||||||
|
<Prompt.Basic
|
||||||
|
control={errorDialogControl}
|
||||||
|
title={_(msg`Oops!`)}
|
||||||
|
description={_(
|
||||||
|
msg`An error occurred while generating your starter pack. Want to try again?`,
|
||||||
|
)}
|
||||||
|
onConfirm={generate}
|
||||||
|
confirmButtonCta={_(msg`Retry`)}
|
||||||
|
/>
|
||||||
|
</LinearGradientBackground>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -453,6 +453,7 @@ function ProfileScreenLoaded({
|
|||||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||||
<ProfileFeedgens
|
<ProfileFeedgens
|
||||||
ref={feedsSectionRef}
|
ref={feedsSectionRef}
|
||||||
|
isMe={isMe}
|
||||||
did={profile.did}
|
did={profile.did}
|
||||||
scrollElRef={scrollElRef as ListRef}
|
scrollElRef={scrollElRef as ListRef}
|
||||||
headerOffset={headerHeight}
|
headerOffset={headerHeight}
|
||||||
|
|||||||
Reference in New Issue
Block a user