rework wizard

This commit is contained in:
Hailey
2024-06-06 00:45:26 -07:00
parent 371a1b84e5
commit e607f45b43
5 changed files with 127 additions and 294 deletions
@@ -1,281 +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 {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 {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 {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'
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 <AddProfiles {...props} />
}
return <AddFeeds {...props} />
}
function AddProfiles(props: Props) {
const [searchText, setSearchText] = useState('')
const {currentAccount} = useSession()
const {data: followsPages, fetchNextPage} = useProfileFollowsQuery(
currentAccount?.did,
)
const follows = followsPages?.pages.flatMap(page => page.follows) || []
const {data: searchedProfiles} = useActorAutocompleteQuery(
searchText,
true,
12,
)
return (
<AddDialog
{...props}
data={searchText ? searchedProfiles : follows}
onEndReached={searchText ? undefined : () => 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 (
<AddDialog
{...props}
data={searchText ? searchedFeeds : popularFeeds}
onEndReached={() => 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<BottomSheetFlatListMethods>(null)
const inputRef = useRef<RNTextInput>(null)
useLayoutEffect(() => {
if (isWeb) {
setImmediate(() => {
inputRef?.current?.focus()
})
}
}, [])
const renderItem = ({item}: ListRenderItemInfo<any>) =>
type === 'profiles' ? (
<WizardProfileCard profile={item} state={state} dispatch={dispatch} />
) : (
<WizardFeedCard generator={item} state={state} dispatch={dispatch} />
)
return (
<Dialog.Outer
control={control}
testID="newChatDialog"
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
<Dialog.InnerFlatList
ref={listRef}
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
ListHeaderComponent={
<ListHeader
searchText={searchText}
setSearchText={setSearchText}
inputRef={inputRef}
type={type}
/>
}
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={onEndReached}
onEndReachedThreshold={2}
removeClippedSubviews={true}
/>
</Dialog.Outer>
)
}
function ListHeader({
type,
searchText,
setSearchText,
inputRef,
}: {
type: 'profiles' | 'feeds'
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,
]}>
{type === 'profiles' ? (
<Trans>Select profiles to add</Trans>
) : (
<Trans>Select feeds 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>
)
}
@@ -0,0 +1,103 @@
import React, {useLayoutEffect, useRef} 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 {Trans} from '@lingui/macro'
import {isWeb} from 'platform/detection'
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, native, useTheme, web} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {WizardFeedCard} from '#/components/StarterPack/Wizard/WizardFeedCard'
import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardProfileCard'
import {Text} from '#/components/Typography'
function keyExtractor(
item: AppBskyActorDefs.ProfileViewBasic | GeneratorView,
index: number,
) {
return `${item.did}-${index}`
}
export function WizardEditListDialog({
control,
state,
dispatch,
}: {
control: Dialog.DialogControlProps
state: WizardState
dispatch: (action: WizardAction) => void
}) {
const t = useTheme()
const listRef = useRef<BottomSheetFlatListMethods>(null)
const inputRef = useRef<RNTextInput>(null)
useLayoutEffect(() => {
if (isWeb) {
setImmediate(() => {
inputRef?.current?.focus()
})
}
}, [])
const renderItem = ({item}: ListRenderItemInfo<any>) =>
state.currentStep === 'Profiles' ? (
<WizardProfileCard profile={item} state={state} dispatch={dispatch} />
) : (
<WizardFeedCard generator={item} state={state} dispatch={dispatch} />
)
return (
<Dialog.Outer
control={control}
testID="newChatDialog"
nativeOptions={{sheet: {snapPoints: ['95%']}}}>
<Dialog.Handle />
<Dialog.InnerFlatList
ref={listRef}
data={state.currentStep === 'Profiles' ? state.profiles : state.feeds}
renderItem={renderItem}
keyExtractor={keyExtractor}
ListHeaderComponent={
<View
style={[
a.align_center,
a.justify_end,
a.border_b,
a.pb_sm,
a.mb_sm,
t.atoms.bg,
t.atoms.border_contrast_medium,
{height: 68},
]}>
<Text style={[a.font_bold, a.text_xl]}>
{state.currentStep === 'Profiles' ? (
<Trans>Edit Profiles</Trans>
) : (
<Trans>Edit Feeds</Trans>
)}
</Text>
</View>
}
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"
removeClippedSubviews={true}
/>
</Dialog.Outer>
)
}
@@ -12,6 +12,7 @@ import {SearchInput} from 'view/com/util/forms/SearchInput'
import {List} from 'view/com/util/List'
import {useWizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a} from '#/alf'
import {Loader} from '#/components/Loader'
import {WizardFeedCard} from '#/components/StarterPack/Wizard/WizardFeedCard'
function keyExtractor(item: GeneratorView) {
@@ -69,6 +70,11 @@ export function StepFeeds() {
onEndReached={!query ? () => fetchNextPage() : undefined}
onEndReachedThreshold={2}
renderScrollComponent={props => <KeyboardAwareScrollView {...props} />}
ListEmptyComponent={
<View style={[a.flex_1, a.align_center, a.mt_lg]}>
<Loader size="lg" />
</View>
}
/>
</>
)
@@ -10,6 +10,7 @@ import {SearchInput} from 'view/com/util/forms/SearchInput'
import {List} from 'view/com/util/List'
import {useWizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a} from '#/alf'
import {Loader} from '#/components/Loader'
import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardProfileCard'
function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) {
@@ -57,6 +58,11 @@ export function StepProfiles() {
onEndReached={!query ? () => fetchNextPage() : undefined}
onEndReachedThreshold={2}
renderScrollComponent={props => <KeyboardAwareScrollView {...props} />}
ListEmptyComponent={
<View style={[a.flex_1, a.align_center, a.mt_lg]}>
<Loader size="lg" />
</View>
}
/>
</>
)
+12 -13
View File
@@ -23,7 +23,7 @@ import {StepProfiles} from '#/screens/StarterPack/Wizard/StepProfiles'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {WizardAddDialog} from '#/components/StarterPack/Wizard/WizardAddDialog'
import {WizardEditListDialog} from '#/components/StarterPack/Wizard/WizardEditListDialog'
import {Text} from '#/components/Typography'
import {Provider} from './State'
@@ -81,7 +81,6 @@ function WizardInner() {
did: currentAccount?.did,
staleTime: 0,
})
const addDialogControl = useDialogControl()
const setMinimalShellMode = useSetMinimalShellMode()
@@ -200,15 +199,6 @@ function WizardInner() {
) : (
<Footer onNext={onNext} nextBtnText={uiStrings.nextBtn} />
)}
{(state.currentStep === 'Profiles' || state.currentStep === 'Feeds') && (
<WizardAddDialog
control={addDialogControl}
state={state}
dispatch={dispatch}
type={state.currentStep === 'Profiles' ? 'profiles' : 'feeds'}
/>
)}
</CenteredView>
)
}
@@ -250,7 +240,8 @@ function Footer({
}) {
const {_} = useLingui()
const t = useTheme()
const [state] = useWizardState()
const [state, dispatch] = useWizardState()
const editDialogControl = useDialogControl()
const items = state.currentStep === 'Profiles' ? state.profiles : state.feeds
@@ -345,7 +336,9 @@ function Footer({
label={_(msg`Edit`)}
variant="ghost"
color="primary"
size="small">
size="small"
disabled={items.length === 0}
onPress={editDialogControl.open}>
<ButtonText>
<Trans>Edit</Trans>
</ButtonText>
@@ -359,6 +352,12 @@ function Footer({
<ButtonText>{nextBtnText}</ButtonText>
</Button>
</View>
<WizardEditListDialog
control={editDialogControl}
state={state}
dispatch={dispatch}
/>
</View>
)
}