From 30686702a61919d8bdaef98313b0b9449469cbdd Mon Sep 17 00:00:00 2001 From: Hailey Date: Sun, 2 Jun 2024 18:14:08 -0700 Subject: [PATCH] =?UTF-8?q?[=F0=9F=8F=81=20#2]=20Starter=20pack=20wizard?= =?UTF-8?q?=20(#4266)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * minimal landing page add a minimal gate remove overlap row add bg to avatar set implement basic mock up add header of mock add route * add navigation * create starter pack landing page * add state reducer * make value an array * implement state w/ steps * add back the flex * view header back button * add button text * add button text * add details screen * add button states * proper use of button * oops * rm extra state * use keyboard aware scrollview * rm log * remove some stuff * remove avatar from landing * add list for profiles * add profile card w/ button * add feeds * add processing state * support editing a pack * fix undefined error * add desktop props * add entire profile to state * scope the profile list better * scoping * add key extractor to feeds * add fab * add icon to details * changes * add dialog for adding profiles * remove log * rm unused iports * fix nav type --- assets/icons/starter_pack_icon.svg | 1 + src/Navigation.tsx | 6 + src/components/forms/TextField.tsx | 2 + src/components/icons/StarterPackIcon.tsx | 26 ++ src/lib/hooks/useBottomBarOffset.ts | 11 + src/lib/routes/types.ts | 10 + src/routes.ts | 1 + src/screens/StarterPack/Wizard/State.tsx | 137 +++++++++++ .../StarterPack/Wizard/StepDetails.tsx | 58 +++++ src/screens/StarterPack/Wizard/StepFeeds.tsx | 88 +++++++ .../StarterPack/Wizard/StepFinished.tsx | 0 .../StarterPack/Wizard/StepLanding.tsx | 33 +++ .../StepProfiles/WizardAddProfilesDialog.tsx | 189 +++++++++++++++ .../Wizard/StepProfiles/WizardProfileCard.tsx | 72 ++++++ .../StarterPack/Wizard/StepProfiles/index.tsx | 52 ++++ src/screens/StarterPack/Wizard/index.tsx | 228 ++++++++++++++++++ src/view/com/util/ViewHeader.tsx | 10 +- src/view/screens/Settings/index.tsx | 12 + 18 files changed, 934 insertions(+), 2 deletions(-) create mode 100644 assets/icons/starter_pack_icon.svg create mode 100644 src/components/icons/StarterPackIcon.tsx create mode 100644 src/lib/hooks/useBottomBarOffset.ts create mode 100644 src/screens/StarterPack/Wizard/State.tsx create mode 100644 src/screens/StarterPack/Wizard/StepDetails.tsx create mode 100644 src/screens/StarterPack/Wizard/StepFeeds.tsx create mode 100644 src/screens/StarterPack/Wizard/StepFinished.tsx create mode 100644 src/screens/StarterPack/Wizard/StepLanding.tsx create mode 100644 src/screens/StarterPack/Wizard/StepProfiles/WizardAddProfilesDialog.tsx create mode 100644 src/screens/StarterPack/Wizard/StepProfiles/WizardProfileCard.tsx create mode 100644 src/screens/StarterPack/Wizard/StepProfiles/index.tsx create mode 100644 src/screens/StarterPack/Wizard/index.tsx diff --git a/assets/icons/starter_pack_icon.svg b/assets/icons/starter_pack_icon.svg new file mode 100644 index 0000000000..47a2f49b64 --- /dev/null +++ b/assets/icons/starter_pack_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Navigation.tsx b/src/Navigation.tsx index dc9ac081cd..f3a3d264f0 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -44,6 +44,7 @@ import HashtagScreen from '#/screens/Hashtag' import {ModerationScreen} from '#/screens/Moderation' import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy' import {Landing} from '#/screens/StarterPack/Landing' +import {Wizard} from '#/screens/StarterPack/Wizard' import {init as initAnalytics} from './lib/analytics/analytics' import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration' import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig' @@ -312,6 +313,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { getComponent={() => Landing} options={{title: title(msg`Join Bluesky Today!`)}} /> + Wizard} + options={{title: title(msg`Create a starter pack`), requireAuth: true}} + /> ) } diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index 73a660ea6c..f543c821e0 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -140,6 +140,7 @@ export function createInput(Component: typeof TextInput) { onChangeText, isInvalid, inputRef, + style, ...rest }: InputProps) { const t = useTheme() @@ -199,6 +200,7 @@ export function createInput(Component: typeof TextInput) { android({ paddingBottom: 16, }), + style, ]} /> diff --git a/src/components/icons/StarterPackIcon.tsx b/src/components/icons/StarterPackIcon.tsx new file mode 100644 index 0000000000..dea27ea85d --- /dev/null +++ b/src/components/icons/StarterPackIcon.tsx @@ -0,0 +1,26 @@ +import * as React from 'react' +import Svg, {Defs, LinearGradient, Path, Stop} from 'react-native-svg' +export function StarterPackIcon({...props}: React.ComponentProps) { + return ( + + + + + + + + + + ) +} diff --git a/src/lib/hooks/useBottomBarOffset.ts b/src/lib/hooks/useBottomBarOffset.ts new file mode 100644 index 0000000000..205378b61b --- /dev/null +++ b/src/lib/hooks/useBottomBarOffset.ts @@ -0,0 +1,11 @@ +import {useSafeAreaInsets} from 'react-native-safe-area-context' + +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {clamp} from 'lib/numbers' +import {isWeb} from 'platform/detection' + +export function useBottomBarOffset() { + const {isTabletOrDesktop} = useWebMediaQueries() + const {bottom: bottomInset} = useSafeAreaInsets() + return isWeb && isTabletOrDesktop ? 0 : clamp(60 + bottomInset, 60, 75) +} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index b19c7bdb64..8c82f44b16 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -41,6 +41,11 @@ export type CommonNavigatorParams = { MessagesConversation: {conversation: string; embed?: string} MessagesSettings: undefined StarterPackLanding: {id: string} + StarterPackWizard: { + mode: 'Create' | 'Edit' + id?: string + initialStep?: 'Details' | 'Profiles' | 'Feeds' + } } export type BottomTabNavigatorParams = CommonNavigatorParams & { @@ -99,6 +104,11 @@ export type AllNavigatorParams = CommonNavigatorParams & { MessagesTab: undefined Messages: {animation?: 'push' | 'pop'} StarterPackLanding: {id: string} + StarterPackWizard: { + mode: 'Create' | 'Edit' + id?: string + initialStep?: 'Details' | 'Profiles' | 'Feeds' + } } // NOTE diff --git a/src/routes.ts b/src/routes.ts index f4b0234b01..98b0b6cbfc 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -41,4 +41,5 @@ export const router = new Router({ MessagesSettings: '/messages/settings', MessagesConversation: '/messages/:conversation', StarterPackLanding: '/start/:id', + StarterPackWizard: '/starter-pack/create', }) diff --git a/src/screens/StarterPack/Wizard/State.tsx b/src/screens/StarterPack/Wizard/State.tsx new file mode 100644 index 0000000000..d82254e43f --- /dev/null +++ b/src/screens/StarterPack/Wizard/State.tsx @@ -0,0 +1,137 @@ +import React from 'react' +import {AppBskyActorDefs} from '@atproto/api' + +const steps = ['Landing', 'Details', 'Profiles', 'Feeds'] as const +type Step = (typeof steps)[number] + +type Action = + | {type: 'Next'} + | {type: 'Back'} + | {type: 'SetCanNext'; canNext: boolean} + | {type: 'SetName'; name: string} + | {type: 'SetDescription'; description: string} + | {type: 'AddProfile'; profile: AppBskyActorDefs.ProfileViewBasic} + | {type: 'RemoveProfile'; profileDid: string} + | {type: 'AddFeed'; uri: string} + | {type: 'RemoveFeed'; uri: string} + | {type: 'SetProcessing'; processing: boolean} + +interface State { + canNext: boolean + currentStep: Step + name?: string + description?: string + avatar?: string + profiles: AppBskyActorDefs.ProfileViewBasic[] + feedUris: string[] + processing: boolean +} + +type TStateContext = [State, (action: Action) => void] + +const StateContext = React.createContext([ + {} as State, + (_: Action) => {}, +]) +export const useWizardState = () => React.useContext(StateContext) + +function reducer(state: State, action: Action): State { + let updatedState = state + + // -- Navigation + if (action.type === 'Next' && state.currentStep !== 'Feeds') { + const currentIndex = steps.indexOf(state.currentStep) + updatedState = {...state, currentStep: steps[currentIndex + 1]} + } else if (action.type === 'Back' && state.currentStep !== 'Landing') { + const currentIndex = steps.indexOf(state.currentStep) + updatedState = {...state, currentStep: steps[currentIndex - 1]} + } + + switch (action.type) { + case 'SetName': + updatedState = {...state, name: action.name} + break + case 'SetDescription': + updatedState = {...state, description: action.description} + break + case 'AddProfile': + updatedState = {...state, profiles: [...state.profiles, action.profile]} + break + case 'RemoveProfile': + updatedState = { + ...state, + profiles: state.profiles.filter( + profile => profile.did !== action.profileDid, + ), + } + break + case 'AddFeed': + updatedState = {...state, feedUris: [...state.feedUris, action.uri]} + break + case 'RemoveFeed': + updatedState = { + ...state, + feedUris: state.feedUris.filter(uri => uri !== action.uri), + } + break + case 'SetProcessing': + updatedState = {...state, processing: action.processing} + break + } + + switch (updatedState.currentStep) { + case 'Landing': + updatedState = { + ...updatedState, + canNext: true, + } + break + case 'Details': + updatedState = { + ...updatedState, + canNext: Boolean(updatedState.description), + } + break + } + + return updatedState +} + +// TODO supply the initial state to this component +export function Provider({ + initialState, + initialStep = 'Landing', + children, +}: { + initialState?: any // TODO update this type + initialStep?: Step + children: React.ReactNode +}) { + const stateAndReducer = React.useReducer( + reducer, + initialState + ? { + ...initialState, + step: initialStep, + } + : { + canNext: true, + currentStep: initialStep, + profiles: [], + feedUris: [], + processing: false, + }, + ) + + return ( + + {children} + + ) +} + +export { + type Action as WizardAction, + type State as WizardState, + type Step as WizardStep, +} diff --git a/src/screens/StarterPack/Wizard/StepDetails.tsx b/src/screens/StarterPack/Wizard/StepDetails.tsx new file mode 100644 index 0000000000..f6388aed60 --- /dev/null +++ b/src/screens/StarterPack/Wizard/StepDetails.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import {View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useProfileQuery} from 'state/queries/profile' +import {useSession} from 'state/session' +import {useWizardState} from '#/screens/StarterPack/Wizard/State' +import {atoms as a} from '#/alf' +import * as TextField from '#/components/forms/TextField' +import {StarterPackIcon} from '#/components/icons/StarterPackIcon' + +export function StepDetails() { + const {_} = useLingui() + const [state, dispatch] = useWizardState() + + const {currentAccount} = useSession() + const {data: currentProfile} = useProfileQuery({ + did: currentAccount?.did, + staleTime: 300, + }) + + return ( + + + + + + {_(msg`Starter pack name`)} + dispatch({type: 'SetName', name: text})} + /> + + + {_(msg`Description`)} + + + dispatch({type: 'SetDescription', description: text}) + } + multiline + style={{minHeight: 150}} + /> + + + + ) +} diff --git a/src/screens/StarterPack/Wizard/StepFeeds.tsx b/src/screens/StarterPack/Wizard/StepFeeds.tsx new file mode 100644 index 0000000000..2b889ae767 --- /dev/null +++ b/src/screens/StarterPack/Wizard/StepFeeds.tsx @@ -0,0 +1,88 @@ +import React from 'react' +import {ListRenderItemInfo, 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 {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 +} + +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}) + } + } + + return ( + + + + + {generator.displayName} + + + {_(msg`Feed by @${generator.creator.handle}`)} + + + + + ) +} diff --git a/src/screens/StarterPack/Wizard/StepFinished.tsx b/src/screens/StarterPack/Wizard/StepFinished.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/screens/StarterPack/Wizard/StepLanding.tsx b/src/screens/StarterPack/Wizard/StepLanding.tsx new file mode 100644 index 0000000000..a6df20eaa5 --- /dev/null +++ b/src/screens/StarterPack/Wizard/StepLanding.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import {atoms as a, useTheme} from '#/alf' +import {StarterPackIcon} from '#/components/icons/StarterPackIcon' +import {Text} from '#/components/Typography' + +export function StepLanding() { + const t = useTheme() + + return ( + + + + + + + Starter packs + + + Invites, but personal + + + + Create your own Bluesky starter packs and invite people directly to + your favorite feeds, profiles, and more. + + + + + ) +} diff --git a/src/screens/StarterPack/Wizard/StepProfiles/WizardAddProfilesDialog.tsx b/src/screens/StarterPack/Wizard/StepProfiles/WizardAddProfilesDialog.tsx new file mode 100644 index 0000000000..d74f24b05d --- /dev/null +++ b/src/screens/StarterPack/Wizard/StepProfiles/WizardAddProfilesDialog.tsx @@ -0,0 +1,189 @@ +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(null) + const inputRef = useRef(null) + + useLayoutEffect(() => { + if (isWeb) { + setImmediate(() => { + inputRef?.current?.focus() + }) + } + }, []) + + const renderItem = ({ + item, + }: ListRenderItemInfo) => { + return ( + + ) + } + + return ( + + 0 ? results : follows} + renderItem={renderItem} + keyExtractor={keyExtractor} + ListHeaderComponent={ + + } + 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} + /> + + ) +} + +function ListHeader({ + searchText, + setSearchText, + inputRef, +}: { + searchText: string + setSearchText: (text: string) => void + inputRef: React.Ref +}) { + 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 ( + + + + Select profiles to add + + + + + + + + + + + + ) +} diff --git a/src/screens/StarterPack/Wizard/StepProfiles/WizardProfileCard.tsx b/src/screens/StarterPack/Wizard/StepProfiles/WizardProfileCard.tsx new file mode 100644 index 0000000000..659de00cb3 --- /dev/null +++ b/src/screens/StarterPack/Wizard/StepProfiles/WizardProfileCard.tsx @@ -0,0 +1,72 @@ +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 ( + + + + + {profile?.displayName || profile?.handle} + + + @{profile?.handle} + + + + + ) +} diff --git a/src/screens/StarterPack/Wizard/StepProfiles/index.tsx b/src/screens/StarterPack/Wizard/StepProfiles/index.tsx new file mode 100644 index 0000000000..84a138ee89 --- /dev/null +++ b/src/screens/StarterPack/Wizard/StepProfiles/index.tsx @@ -0,0 +1,52 @@ +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' + +function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) { + return item.did +} + +export function StepProfiles() { + const [state, dispatch] = useWizardState() + + const renderItem = ({ + item, + }: ListRenderItemInfo) => { + return ( + + ) + } + + 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 new file mode 100644 index 0000000000..080a0cefd7 --- /dev/null +++ b/src/screens/StarterPack/Wizard/index.tsx @@ -0,0 +1,228 @@ +import React from 'react' +import {Keyboard, View} from 'react-native' +import { + KeyboardAwareScrollView, + KeyboardStickyView, +} from 'react-native-keyboard-controller' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' +import {NativeStackScreenProps} from '@react-navigation/native-stack' + +import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset' +import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types' +import {useProfileQuery} from 'state/queries/profile' +import {useSession} from 'state/session' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {CenteredView} from 'view/com/util/Views' +import {useWizardState, WizardStep} from '#/screens/StarterPack/Wizard/State' +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 {Provider} from './State' + +export function Wizard({ + route, +}: NativeStackScreenProps) { + const params = route.params + const {mode, initialStep, id} = params ?? {} + + // TODO load query here + const starterPack = {} + + // TODO use this to wait for loading the starterpack for editing + if (mode === 'Edit' && false) { + // Await here + return + } + + return ( + + ) +} + +function WizardReady({ + mode, + initialStep, + starterPack, +}: { + mode: 'Create' | 'Edit' + id?: string + initialStep?: 'Details' | 'Profiles' | 'Feeds' + starterPack?: any +}) { + return ( + + + + ) +} + +function WizardInner() { + const navigation = useNavigation() + const {_} = useLingui() + const t = useTheme() + const bottomOffset = useBottomBarOffset() + const [state, dispatch] = useWizardState() + const {currentAccount} = useSession() + const {data: currentProfile} = useProfileQuery({ + did: currentAccount?.did, + staleTime: 0, + }) + const bottomBarOffset = useBottomBarOffset() + const addProfilesControl = useDialogControl() + + React.useEffect(() => { + navigation.setOptions({ + gestureEnabled: false, + }) + }, [navigation]) + + const wizardUiStrings: Record = + { + Landing: { + header: _(msg`Create a starter pack`), + button: _(msg`Create`), + }, + Details: { + header: _(msg`Details`), + button: _(msg`Add profiles`), + }, + Profiles: { + header: _(msg`Add profiles`), + button: _(msg`Add feeds`), + }, + Feeds: { + header: _(msg`Add feeds`), + button: _(msg`Finish`), + }, + } + + const uiStrings = wizardUiStrings[state.currentStep] + + const onNext = () => { + if (state.currentStep === 'Details' && !state.name) { + dispatch({ + type: 'SetName', + name: _( + msg`${currentProfile?.displayName || currentProfile?.handle}'s`, + ), + }) + } else if (state.currentStep === 'Feeds') { + dispatch({type: 'SetProcessing', processing: true}) + return + } + + const keyboardVisible = Keyboard.isVisible() + Keyboard.dismiss() + setTimeout( + () => { + dispatch({type: 'Next'}) + }, + keyboardVisible ? 16 : 0, + ) + } + + return ( + + dispatch({type: 'Back'}) + : undefined + } + showBorder={true} + showOnDesktop={true} + renderButton={ + state.currentStep === 'Profiles' + ? () => ( + + ) + : undefined + } + /> + + + + + + + + + + + + ) +} + +function Container({children}: {children: React.ReactNode}) { + const [state] = useWizardState() + + if (state.currentStep === 'Profiles' || state.currentStep === 'Feeds') { + return {children} + } + + return ( + + {children} + + ) +} + +function StepView() { + const [state] = useWizardState() + + if (state.currentStep === 'Landing') { + return + } + if (state.currentStep === 'Details') { + return + } + if (state.currentStep === 'Profiles') { + return + } + if (state.currentStep === 'Feeds') { + return + } +} diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index 4c0f0e3e5c..1147339e54 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -28,6 +28,7 @@ export function ViewHeader({ showOnDesktop, showBorder, renderButton, + onBackPress: onPressBackOverride, }: { title: string subtitle?: string @@ -37,6 +38,7 @@ export function ViewHeader({ showOnDesktop?: boolean showBorder?: boolean renderButton?: () => JSX.Element + onBackPress?: () => void }) { const pal = usePalette('default') const {_} = useLingui() @@ -48,11 +50,15 @@ export function ViewHeader({ const onPressBack = React.useCallback(() => { if (navigation.canGoBack()) { - navigation.goBack() + if (typeof onPressBackOverride === 'function') { + onPressBackOverride() + } else { + navigation.goBack() + } } else { navigation.navigate('Home') } - }, [navigation]) + }, [navigation, onPressBackOverride]) const onPressMenu = React.useCallback(() => { track('ViewHeader:MenuButtonClicked') diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx index f3ecc40d1c..48727a98b4 100644 --- a/src/view/screens/Settings/index.tsx +++ b/src/view/screens/Settings/index.tsx @@ -876,6 +876,18 @@ export function SettingsScreen({}: Props) { Delete chat declaration record + + navigation.navigate('StarterPackWizard', {mode: 'Create'}) + } + accessibilityRole="button" + accessibilityLabel={_(msg`Navigate to Starter Pack Wizard`)} + accessibilityHint={_(msg`Navigates to Starter Pack Wizard`)}> + + Navigate to Starter Pack Wizard + +