* refactor name * refactor to support feeds search * more refactor * cleanup * allow searching for feeds * better empty states * move some stuff around * one more move * rename buttons * another move * clarify variable names
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||
|
||||
const steps = ['Landing', 'Details', 'Profiles', 'Feeds'] as const
|
||||
type Step = (typeof steps)[number]
|
||||
@@ -12,8 +13,8 @@ type Action =
|
||||
| {type: 'SetDescription'; description: string}
|
||||
| {type: 'AddProfile'; profile: AppBskyActorDefs.ProfileViewBasic}
|
||||
| {type: 'RemoveProfile'; profileDid: string}
|
||||
| {type: 'AddFeed'; uri: string}
|
||||
| {type: 'RemoveFeed'; uri: string}
|
||||
| {type: 'AddFeed'; feed: GeneratorView}
|
||||
| {type: 'RemoveFeed'; feedUri: string}
|
||||
| {type: 'SetProcessing'; processing: boolean}
|
||||
|
||||
interface State {
|
||||
@@ -23,7 +24,7 @@ interface State {
|
||||
description?: string
|
||||
avatar?: string
|
||||
profiles: AppBskyActorDefs.ProfileViewBasic[]
|
||||
feedUris: string[]
|
||||
feeds: GeneratorView[]
|
||||
processing: boolean
|
||||
}
|
||||
|
||||
@@ -66,12 +67,12 @@ function reducer(state: State, action: Action): State {
|
||||
}
|
||||
break
|
||||
case 'AddFeed':
|
||||
updatedState = {...state, feedUris: [...state.feedUris, action.uri]}
|
||||
updatedState = {...state, feeds: [...state.feeds, action.feed]}
|
||||
break
|
||||
case 'RemoveFeed':
|
||||
updatedState = {
|
||||
...state,
|
||||
feedUris: state.feedUris.filter(uri => uri !== action.uri),
|
||||
feeds: state.feeds.filter(f => f.uri !== action.feedUri),
|
||||
}
|
||||
break
|
||||
case 'SetProcessing':
|
||||
@@ -118,7 +119,7 @@ export function Provider({
|
||||
canNext: true,
|
||||
currentStep: initialStep,
|
||||
profiles: [],
|
||||
feedUris: [],
|
||||
feeds: [],
|
||||
processing: false,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,88 +1,33 @@
|
||||
import React from 'react'
|
||||
import {ListRenderItemInfo, View} from 'react-native'
|
||||
import {ListRenderItemInfo} from 'react-native'
|
||||
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useProfileFeedgensQuery} from 'state/queries/profile-feedgens'
|
||||
import {useSession} from 'state/session'
|
||||
import {List} from 'view/com/util/List'
|
||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {useWizardState} from '#/screens/StarterPack/Wizard/State'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
function renderItem({item}: ListRenderItemInfo<GeneratorView>) {
|
||||
return <FeedCard generator={item} />
|
||||
}
|
||||
import {atoms as a} from '#/alf'
|
||||
import {WizardFeedCard} from '#/components/StarterPack/Wizard/WizardFeedCard'
|
||||
import {WizardListEmpty} from '#/components/StarterPack/Wizard/WizardListEmpty'
|
||||
|
||||
function keyExtractor(item: GeneratorView) {
|
||||
return item.uri
|
||||
}
|
||||
|
||||
export function StepFeeds() {
|
||||
const {currentAccount} = useSession()
|
||||
const {data} = useProfileFeedgensQuery(currentAccount!.did)
|
||||
const feeds = data?.pages.flatMap(page => page.feeds) || []
|
||||
|
||||
return (
|
||||
<List
|
||||
data={feeds}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
style={[a.flex_1]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FeedCard({generator}: {generator: GeneratorView}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const [state, dispatch] = useWizardState()
|
||||
|
||||
const includesFeed = state.feedUris.includes(generator.uri)
|
||||
const onAdd = () => {
|
||||
if (includesFeed) {
|
||||
dispatch({type: 'RemoveFeed', uri: generator.uri})
|
||||
} else {
|
||||
dispatch({type: 'AddFeed', uri: generator.uri})
|
||||
}
|
||||
console.log(state)
|
||||
|
||||
const renderItem = ({item}: ListRenderItemInfo<GeneratorView>) => {
|
||||
return <WizardFeedCard generator={item} state={state} dispatch={dispatch} />
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.px_md,
|
||||
a.py_sm,
|
||||
a.border_b,
|
||||
a.gap_md,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<UserAvatar type="algo" size={45} avatar={generator.avatar} />
|
||||
<View style={[a.flex_1]}>
|
||||
<Text style={[a.flex_1, a.font_bold, a.text_md]} numberOfLines={1}>
|
||||
{generator.displayName}
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.flex_1, t.atoms.text_contrast_medium]}
|
||||
numberOfLines={1}>
|
||||
{_(msg`Feed by @${generator.creator.handle}`)}
|
||||
</Text>
|
||||
</View>
|
||||
<Button
|
||||
label={includesFeed ? _(msg`Remove`) : _(msg`Add`)}
|
||||
variant="solid"
|
||||
color={includesFeed ? 'secondary' : 'primary'}
|
||||
size="small"
|
||||
style={{paddingVertical: 6}}
|
||||
onPress={onAdd}>
|
||||
<ButtonText>
|
||||
{includesFeed ? <Trans>Remove</Trans> : <Trans>Add</Trans>}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
<List
|
||||
data={state.feeds}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
style={[a.flex_1]}
|
||||
ListEmptyComponent={<WizardListEmpty type="feeds" />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
+8
-22
@@ -1,13 +1,12 @@
|
||||
import React from 'react'
|
||||
import {ListRenderItemInfo, View} from 'react-native'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {List} from 'view/com/util/List'
|
||||
import {useWizardState} from '#/screens/StarterPack/Wizard/State'
|
||||
import {WizardProfileCard} from '#/screens/StarterPack/Wizard/StepProfiles/WizardProfileCard'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {WizardListEmpty} from '#/components/StarterPack/Wizard/WizardListEmpty'
|
||||
import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardProfileCard'
|
||||
|
||||
function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) {
|
||||
return item.did
|
||||
@@ -27,26 +26,13 @@ export function StepProfiles() {
|
||||
return (
|
||||
<>
|
||||
<View style={[a.flex_1]}>
|
||||
{state.profiles.length > 0 ? (
|
||||
<List
|
||||
data={state.profiles}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
/>
|
||||
) : (
|
||||
<ListEmpty />
|
||||
)}
|
||||
<List
|
||||
data={state.profiles}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
ListEmptyComponent={<WizardListEmpty type="profiles" />}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ListEmpty() {
|
||||
return (
|
||||
<View style={[a.flex_1, a.px_md, a.align_center]}>
|
||||
<Text style={[a.font_bold, a.text_xl, a.text_center, {marginTop: 100}]}>
|
||||
<Trans>Add the people you recommend to your starter pack!</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
import React, {useLayoutEffect, useRef, useState} from 'react'
|
||||
import type {ListRenderItemInfo, TextInput as RNTextInput} from 'react-native'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
|
||||
import {useProfileFollowsQuery} from 'state/queries/profile-follows'
|
||||
import {useSession} from 'state/session'
|
||||
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
|
||||
import {WizardProfileCard} from '#/screens/StarterPack/Wizard/StepProfiles/WizardProfileCard'
|
||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {TextInput} from '#/components/dms/dialogs/TextInput'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) {
|
||||
return item.did
|
||||
}
|
||||
|
||||
export function WizardAddProfilesDialog({
|
||||
control,
|
||||
state,
|
||||
dispatch,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
state: WizardState
|
||||
dispatch: (action: WizardAction) => void
|
||||
}) {
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
||||
const {currentAccount} = useSession()
|
||||
const {data: results} = useActorAutocompleteQuery(searchText, true, 12)
|
||||
const {data: followsPages, fetchNextPage} = useProfileFollowsQuery(
|
||||
currentAccount?.did,
|
||||
)
|
||||
const follows = followsPages?.pages.flatMap(page => page.follows) || []
|
||||
|
||||
const listRef = useRef<BottomSheetFlatListMethods>(null)
|
||||
const inputRef = useRef<RNTextInput>(null)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isWeb) {
|
||||
setImmediate(() => {
|
||||
inputRef?.current?.focus()
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const renderItem = ({
|
||||
item,
|
||||
}: ListRenderItemInfo<AppBskyActorDefs.ProfileViewBasic>) => {
|
||||
return (
|
||||
<WizardProfileCard profile={item} state={state} dispatch={dispatch} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
testID="newChatDialog"
|
||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
|
||||
<Dialog.InnerFlatList
|
||||
ref={listRef}
|
||||
data={searchText.length > 0 ? results : follows}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
ListHeaderComponent={
|
||||
<ListHeader
|
||||
searchText={searchText}
|
||||
setSearchText={setSearchText}
|
||||
inputRef={inputRef}
|
||||
/>
|
||||
}
|
||||
stickyHeaderIndices={[0]}
|
||||
style={[
|
||||
web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
|
||||
native({
|
||||
height: '100%',
|
||||
paddingHorizontal: 0,
|
||||
marginTop: 0,
|
||||
paddingTop: 0,
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
}),
|
||||
]}
|
||||
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
|
||||
keyboardDismissMode="on-drag"
|
||||
onEndReached={() => fetchNextPage()}
|
||||
onEndReachedThreshold={2}
|
||||
removeClippedSubviews={true}
|
||||
/>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function ListHeader({
|
||||
searchText,
|
||||
setSearchText,
|
||||
inputRef,
|
||||
}: {
|
||||
searchText: string
|
||||
setSearchText: (text: string) => void
|
||||
inputRef: React.Ref<RNTextInput>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onMouseEnter,
|
||||
onOut: onMouseLeave,
|
||||
} = useInteractionState()
|
||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
const interacted = hovered || focused
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.pt_md,
|
||||
a.pb_xs,
|
||||
a.px_lg,
|
||||
a.border_b,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
native([a.pt_lg]),
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
native(a.align_center),
|
||||
a.justify_center,
|
||||
{height: 32},
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.z_10,
|
||||
a.text_lg,
|
||||
a.font_bold,
|
||||
a.leading_tight,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>Select profiles to add</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={[native([a.pt_sm]), web([a.pt_xs])]}>
|
||||
<View
|
||||
{...web({
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
})}
|
||||
style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Search
|
||||
size="md"
|
||||
fill={interacted ? t.palette.primary_500 : t.palette.contrast_300}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
// @ts-ignore bottom sheet input types issue — esb
|
||||
ref={inputRef}
|
||||
placeholder={_(msg`Search`)}
|
||||
value={searchText}
|
||||
onChangeText={setSearchText}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={[a.flex_1, a.py_md, a.text_md, t.atoms.text]}
|
||||
placeholderTextColor={t.palette.contrast_500}
|
||||
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
|
||||
returnKeyType="search"
|
||||
clearButtonMode="while-editing"
|
||||
maxLength={50}
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
autoFocus
|
||||
accessibilityLabel={_(msg`Search profiles`)}
|
||||
accessibilityHint={_(msg`Search profiles`)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function WizardProfileCard({
|
||||
state,
|
||||
dispatch,
|
||||
profile,
|
||||
}: {
|
||||
state: WizardState
|
||||
dispatch: (action: WizardAction) => void
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
const includesProfile = state.profiles.some(p => p.did === profile.did)
|
||||
|
||||
const onPressAddRemove = () => {
|
||||
if (!profile?.did) return
|
||||
|
||||
if (!includesProfile) {
|
||||
dispatch({type: 'AddProfile', profile})
|
||||
} else {
|
||||
dispatch({type: 'RemoveProfile', profileDid: profile.did})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.px_md,
|
||||
a.py_sm,
|
||||
a.gap_md,
|
||||
a.border_b,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<UserAvatar size={45} avatar={profile?.avatar} />
|
||||
<View style={[a.flex_1]}>
|
||||
<Text style={[a.flex_1, a.font_bold, a.text_md]} numberOfLines={1}>
|
||||
{profile?.displayName || profile?.handle}
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.flex_1, t.atoms.text_contrast_medium]}
|
||||
numberOfLines={1}>
|
||||
@{profile?.handle}
|
||||
</Text>
|
||||
</View>
|
||||
<Button
|
||||
label={includesProfile ? _(msg`Remove`) : _(msg`Add`)}
|
||||
variant="solid"
|
||||
color={includesProfile ? 'secondary' : 'primary'}
|
||||
size="small"
|
||||
style={{paddingVertical: 6}}
|
||||
onPress={onPressAddRemove}>
|
||||
<ButtonText>
|
||||
{includesProfile ? <Trans>Remove</Trans> : <Trans>Add</Trans>}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -20,11 +20,11 @@ import {StepDetails} from '#/screens/StarterPack/Wizard/StepDetails'
|
||||
import {StepFeeds} from '#/screens/StarterPack/Wizard/StepFeeds'
|
||||
import {StepLanding} from '#/screens/StarterPack/Wizard/StepLanding'
|
||||
import {StepProfiles} from '#/screens/StarterPack/Wizard/StepProfiles'
|
||||
import {WizardAddProfilesDialog} from '#/screens/StarterPack/Wizard/StepProfiles/WizardAddProfilesDialog'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {WizardAddDialog} from '#/components/StarterPack/Wizard/WizardAddDialog'
|
||||
import {Provider} from './State'
|
||||
|
||||
export function Wizard({
|
||||
@@ -83,7 +83,7 @@ function WizardInner() {
|
||||
staleTime: 0,
|
||||
})
|
||||
const bottomBarOffset = useBottomBarOffset()
|
||||
const addProfilesControl = useDialogControl()
|
||||
const searchDialogControl = useDialogControl()
|
||||
|
||||
React.useEffect(() => {
|
||||
navigation.setOptions({
|
||||
@@ -95,15 +95,15 @@ function WizardInner() {
|
||||
{
|
||||
Landing: {
|
||||
header: _(msg`Create a starter pack`),
|
||||
button: _(msg`Create`),
|
||||
button: _(msg`Get started`),
|
||||
},
|
||||
Details: {
|
||||
header: _(msg`Details`),
|
||||
button: _(msg`Add profiles`),
|
||||
button: _(msg`Continue`),
|
||||
},
|
||||
Profiles: {
|
||||
header: _(msg`Add profiles`),
|
||||
button: _(msg`Add feeds`),
|
||||
button: _(msg`Continue`),
|
||||
},
|
||||
Feeds: {
|
||||
header: _(msg`Add feeds`),
|
||||
@@ -150,14 +150,14 @@ function WizardInner() {
|
||||
showBorder={true}
|
||||
showOnDesktop={true}
|
||||
renderButton={
|
||||
state.currentStep === 'Profiles'
|
||||
state.currentStep === 'Profiles' || state.currentStep === 'Feeds'
|
||||
? () => (
|
||||
<Button
|
||||
label={_(msg`Cancel`)}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="xsmall"
|
||||
onPress={addProfilesControl.open}
|
||||
onPress={searchDialogControl.open}
|
||||
style={{marginLeft: -15}}>
|
||||
<ButtonText>
|
||||
<Trans>Add</Trans>
|
||||
@@ -187,11 +187,14 @@ function WizardInner() {
|
||||
</View>
|
||||
</KeyboardStickyView>
|
||||
|
||||
<WizardAddProfilesDialog
|
||||
control={addProfilesControl}
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
{(state.currentStep === 'Profiles' || state.currentStep === 'Feeds') && (
|
||||
<WizardAddDialog
|
||||
control={searchDialogControl}
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
type={state.currentStep === 'Profiles' ? 'profiles' : 'feeds'}
|
||||
/>
|
||||
)}
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user