diff --git a/src/screens/StarterPack/Wizard/StepProfiles/WizardAddProfilesDialog.tsx b/src/components/StarterPack/Wizard/WizardAddDialog.tsx similarity index 64% rename from src/screens/StarterPack/Wizard/StepProfiles/WizardAddProfilesDialog.tsx rename to src/components/StarterPack/Wizard/WizardAddDialog.tsx index d74f24b05d..1dec85da28 100644 --- a/src/screens/StarterPack/Wizard/StepProfiles/WizardAddProfilesDialog.tsx +++ b/src/components/StarterPack/Wizard/WizardAddDialog.tsx @@ -2,45 +2,131 @@ 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 {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' import {BottomSheetFlatListMethods} from '@discord/bottom-sheet' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import debounce from 'lodash.debounce' -import {isWeb} from '#/platform/detection' +import {isWeb} from 'platform/detection' import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete' +import { + useGetPopularFeedsQuery, + useSearchPopularFeedsMutation, +} from 'state/queries/feed' 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 {WizardFeedCard} from '#/components/StarterPack/Wizard/WizardFeedCard' +import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardProfileCard' import {Text} from '#/components/Typography' -function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) { - return item.did -} - -export function WizardAddProfilesDialog({ - control, - state, - dispatch, -}: { +interface Props { control: Dialog.DialogControlProps + type: 'profiles' | 'feeds' state: WizardState dispatch: (action: WizardAction) => void -}) { +} + +function keyExtractor( + item: AppBskyActorDefs.ProfileViewBasic | GeneratorView, + index: number, +) { + return `${item.did}-${index}` +} + +export function WizardAddDialog(props: Props) { + if (props.type === 'profiles') { + return + } + return +} + +function AddProfiles(props: Props) { 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 {data: searchedProfiles} = useActorAutocompleteQuery( + searchText, + true, + 12, + ) + + return ( + fetchNextPage()} + searchText={searchText} + setSearchText={setSearchText} + /> + ) +} + +function AddFeeds(props: Props) { + const [searchText, setSearchText] = useState('') + + const {data: popularFeedsPages, fetchNextPage} = useGetPopularFeedsQuery() + + const popularFeeds = + popularFeedsPages?.pages.flatMap(page => page.feeds) || [] + + const { + data: searchedFeeds, + mutate: search, + reset: resetSearch, + } = useSearchPopularFeedsMutation() + + const debouncedSearch = React.useMemo( + () => debounce(q => search(q), 500), // debounce for 500ms + [search], + ) + + const onChangeText = (text: string) => { + setSearchText(text) + if (text.length > 1) { + debouncedSearch(text) + } else { + resetSearch() + } + } + + return ( + fetchNextPage()} + searchText={searchText} + setSearchText={onChangeText} + /> + ) +} + +function AddDialog({ + type, + control, + state, + dispatch, + data, + onEndReached, + searchText, + setSearchText, +}: Props & { + data?: AppBskyActorDefs.ProfileViewBasic[] | GeneratorView[] + onEndReached?: () => void + searchText: string + setSearchText: (text: string) => void +}) { const listRef = useRef(null) const inputRef = useRef(null) @@ -52,13 +138,12 @@ export function WizardAddProfilesDialog({ } }, []) - const renderItem = ({ - item, - }: ListRenderItemInfo) => { - return ( + const renderItem = ({item}: ListRenderItemInfo) => + type === 'profiles' ? ( + ) : ( + ) - } return ( 0 ? results : follows} + data={data} renderItem={renderItem} keyExtractor={keyExtractor} ListHeaderComponent={ @@ -75,6 +160,7 @@ export function WizardAddProfilesDialog({ searchText={searchText} setSearchText={setSearchText} inputRef={inputRef} + type={type} /> } stickyHeaderIndices={[0]} @@ -91,7 +177,7 @@ export function WizardAddProfilesDialog({ ]} webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]} keyboardDismissMode="on-drag" - onEndReached={() => fetchNextPage()} + onEndReached={onEndReached} onEndReachedThreshold={2} removeClippedSubviews={true} /> @@ -100,10 +186,12 @@ export function WizardAddProfilesDialog({ } function ListHeader({ + type, searchText, setSearchText, inputRef, }: { + type: 'profiles' | 'feeds' searchText: string setSearchText: (text: string) => void inputRef: React.Ref @@ -145,7 +233,11 @@ function ListHeader({ a.leading_tight, t.atoms.text_contrast_high, ]}> - Select profiles to add + {type === 'profiles' ? ( + Select profiles to add + ) : ( + Select feeds to add + )} diff --git a/src/components/StarterPack/Wizard/WizardFeedCard.tsx b/src/components/StarterPack/Wizard/WizardFeedCard.tsx new file mode 100644 index 0000000000..bf01b3bd4c --- /dev/null +++ b/src/components/StarterPack/Wizard/WizardFeedCard.tsx @@ -0,0 +1,69 @@ +import React from 'react' +import {View} 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 {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 WizardFeedCard({ + generator, + state, + dispatch, +}: { + generator: GeneratorView + state: WizardState + dispatch: (action: WizardAction) => void +}) { + const {_} = useLingui() + const t = useTheme() + + const includesFeed = state.feeds.some(f => f.uri === generator.uri) + const onAdd = () => { + if (includesFeed) { + dispatch({type: 'RemoveFeed', feedUri: generator.uri}) + } else { + dispatch({type: 'AddFeed', feed: generator}) + } + } + + return ( + + + + + {generator.displayName} + + + {_(msg`Feed by @${generator.creator.handle}`)} + + + + + ) +} diff --git a/src/components/StarterPack/Wizard/WizardListEmpty.tsx b/src/components/StarterPack/Wizard/WizardListEmpty.tsx new file mode 100644 index 0000000000..332b89e132 --- /dev/null +++ b/src/components/StarterPack/Wizard/WizardListEmpty.tsx @@ -0,0 +1,44 @@ +import React from 'react' +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import {atoms as a, useTheme} from '#/alf' +import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' +import {UserCircle_Stroke2_Corner0_Rounded as UserCircle} from '#/components/icons/UserCircle' +import {Text} from '#/components/Typography' + +export function WizardListEmpty({type}: {type: 'profiles' | 'feeds'}) { + const t = useTheme() + + return ( + + {type === 'profiles' ? ( + + ) : ( + + )} + + {type === 'profiles' ? ( + Recommend people to follow! + ) : ( + Add some cool feeds! + )} + + + ) +} diff --git a/src/screens/StarterPack/Wizard/StepProfiles/WizardProfileCard.tsx b/src/components/StarterPack/Wizard/WizardProfileCard.tsx similarity index 100% rename from src/screens/StarterPack/Wizard/StepProfiles/WizardProfileCard.tsx rename to src/components/StarterPack/Wizard/WizardProfileCard.tsx diff --git a/src/screens/StarterPack/Wizard/State.tsx b/src/screens/StarterPack/Wizard/State.tsx index d82254e43f..5468c07ead 100644 --- a/src/screens/StarterPack/Wizard/State.tsx +++ b/src/screens/StarterPack/Wizard/State.tsx @@ -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, }, ) diff --git a/src/screens/StarterPack/Wizard/StepFeeds.tsx b/src/screens/StarterPack/Wizard/StepFeeds.tsx index 2b889ae767..8acc8ca262 100644 --- a/src/screens/StarterPack/Wizard/StepFeeds.tsx +++ b/src/screens/StarterPack/Wizard/StepFeeds.tsx @@ -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) { - return -} +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 ( - - ) -} - -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) => { + return } return ( - - - - - {generator.displayName} - - - {_(msg`Feed by @${generator.creator.handle}`)} - - - - + } + /> ) } diff --git a/src/screens/StarterPack/Wizard/StepProfiles/index.tsx b/src/screens/StarterPack/Wizard/StepProfiles.tsx similarity index 51% rename from src/screens/StarterPack/Wizard/StepProfiles/index.tsx rename to src/screens/StarterPack/Wizard/StepProfiles.tsx index 84a138ee89..43211339cc 100644 --- a/src/screens/StarterPack/Wizard/StepProfiles/index.tsx +++ b/src/screens/StarterPack/Wizard/StepProfiles.tsx @@ -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 ( <> - {state.profiles.length > 0 ? ( - - ) : ( - - )} + } + /> ) } - -function ListEmpty() { - return ( - - - Add the people you recommend to your starter pack! - - - ) -} diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx index 080a0cefd7..757277fcd9 100644 --- a/src/screens/StarterPack/Wizard/index.tsx +++ b/src/screens/StarterPack/Wizard/index.tsx @@ -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' ? () => (