* 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
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 153 133"><path fill="url(#a)" fill-rule="evenodd" d="m60.196 105.445-18.1 4.85c-11.73 3.143-23.788-3.819-26.931-15.55L1.19 42.597c-3.143-11.731 3.819-23.79 15.55-26.932L68.889 1.69C80.62-1.452 92.68 5.51 95.821 17.241l4.667 17.416a49.7 49.7 0 0 1 3.522-.125c27.053 0 48.984 21.931 48.984 48.984S131.063 132.5 104.01 132.5c-19.17 0-35.769-11.012-43.814-27.055ZM19.457 25.804 71.606 11.83c6.131-1.643 12.434 1.996 14.076 8.127l4.44 16.571c-20.289 5.987-35.096 24.758-35.096 46.988 0 4.157.517 8.193 1.492 12.047l-17.138 4.593c-6.131 1.642-12.434-1.996-14.077-8.128L11.33 39.88c-1.643-6.131 1.996-12.434 8.127-14.077Zm83.812 19.232c.246-.005.493-.007.741-.007 21.256 0 38.487 17.231 38.487 38.487s-17.231 38.488-38.487 38.488c-14.29 0-26.76-7.788-33.4-19.35l23.635-6.333c11.731-3.143 18.693-15.2 15.55-26.932l-6.526-24.353Zm-10.428 1.638 6.815 25.432c1.642 6.131-1.996 12.434-8.128 14.076l-24.867 6.664a38.57 38.57 0 0 1-1.139-9.33c0-17.372 11.51-32.056 27.32-36.842Z" clip-rule="evenodd"/><defs><linearGradient id="a" x1="76.715" x2="76.715" y1=".937" y2="132.5" gradientUnits="userSpaceOnUse"><stop stop-color="#0A7AFF"/><stop offset="1" stop-color="#59B9FF"/></linearGradient></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -44,6 +44,7 @@ import HashtagScreen from '#/screens/Hashtag'
|
|||||||
import {ModerationScreen} from '#/screens/Moderation'
|
import {ModerationScreen} from '#/screens/Moderation'
|
||||||
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
|
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
|
||||||
import {Landing} from '#/screens/StarterPack/Landing'
|
import {Landing} from '#/screens/StarterPack/Landing'
|
||||||
|
import {Wizard} from '#/screens/StarterPack/Wizard'
|
||||||
import {init as initAnalytics} from './lib/analytics/analytics'
|
import {init as initAnalytics} from './lib/analytics/analytics'
|
||||||
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
|
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
|
||||||
import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig'
|
import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig'
|
||||||
@@ -312,6 +313,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||||||
getComponent={() => Landing}
|
getComponent={() => Landing}
|
||||||
options={{title: title(msg`Join Bluesky Today!`)}}
|
options={{title: title(msg`Join Bluesky Today!`)}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="StarterPackWizard"
|
||||||
|
getComponent={() => Wizard}
|
||||||
|
options={{title: title(msg`Create a starter pack`), requireAuth: true}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ export function createInput(Component: typeof TextInput) {
|
|||||||
onChangeText,
|
onChangeText,
|
||||||
isInvalid,
|
isInvalid,
|
||||||
inputRef,
|
inputRef,
|
||||||
|
style,
|
||||||
...rest
|
...rest
|
||||||
}: InputProps) {
|
}: InputProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -199,6 +200,7 @@ export function createInput(Component: typeof TextInput) {
|
|||||||
android({
|
android({
|
||||||
paddingBottom: 16,
|
paddingBottom: 16,
|
||||||
}),
|
}),
|
||||||
|
style,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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<typeof Svg>) {
|
||||||
|
return (
|
||||||
|
<Svg fill="none" viewBox="0 0 153 133" {...props}>
|
||||||
|
<Path
|
||||||
|
fill="url(#a)"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="m60.196 105.445-18.1 4.85c-11.73 3.143-23.788-3.819-26.931-15.55L1.19 42.597c-3.143-11.731 3.819-23.79 15.55-26.932L68.889 1.69C80.62-1.452 92.68 5.51 95.821 17.241l4.667 17.416a49.7 49.7 0 0 1 3.522-.125c27.053 0 48.984 21.931 48.984 48.984S131.063 132.5 104.01 132.5c-19.17 0-35.769-11.012-43.814-27.055ZM19.457 25.804 71.606 11.83c6.131-1.643 12.434 1.996 14.076 8.127l4.44 16.571c-20.289 5.987-35.096 24.758-35.096 46.988 0 4.157.517 8.193 1.492 12.047l-17.138 4.593c-6.131 1.642-12.434-1.996-14.077-8.128L11.33 39.88c-1.643-6.131 1.996-12.434 8.127-14.077Zm83.812 19.232c.246-.005.493-.007.741-.007 21.256 0 38.487 17.231 38.487 38.487s-17.231 38.488-38.487 38.488c-14.29 0-26.76-7.788-33.4-19.35l23.635-6.333c11.731-3.143 18.693-15.2 15.55-26.932l-6.526-24.353Zm-10.428 1.638 6.815 25.432c1.642 6.131-1.996 12.434-8.128 14.076l-24.867 6.664a38.57 38.57 0 0 1-1.139-9.33c0-17.372 11.51-32.056 27.32-36.842Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
<Defs>
|
||||||
|
<LinearGradient
|
||||||
|
id="a"
|
||||||
|
x1={76.715}
|
||||||
|
x2={76.715}
|
||||||
|
y1={0.937}
|
||||||
|
y2={132.5}
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<Stop stopColor="#0A7AFF" />
|
||||||
|
<Stop offset={1} stopColor="#59B9FF" />
|
||||||
|
</LinearGradient>
|
||||||
|
</Defs>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -41,6 +41,11 @@ export type CommonNavigatorParams = {
|
|||||||
MessagesConversation: {conversation: string; embed?: string}
|
MessagesConversation: {conversation: string; embed?: string}
|
||||||
MessagesSettings: undefined
|
MessagesSettings: undefined
|
||||||
StarterPackLanding: {id: string}
|
StarterPackLanding: {id: string}
|
||||||
|
StarterPackWizard: {
|
||||||
|
mode: 'Create' | 'Edit'
|
||||||
|
id?: string
|
||||||
|
initialStep?: 'Details' | 'Profiles' | 'Feeds'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BottomTabNavigatorParams = CommonNavigatorParams & {
|
export type BottomTabNavigatorParams = CommonNavigatorParams & {
|
||||||
@@ -99,6 +104,11 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
|||||||
MessagesTab: undefined
|
MessagesTab: undefined
|
||||||
Messages: {animation?: 'push' | 'pop'}
|
Messages: {animation?: 'push' | 'pop'}
|
||||||
StarterPackLanding: {id: string}
|
StarterPackLanding: {id: string}
|
||||||
|
StarterPackWizard: {
|
||||||
|
mode: 'Create' | 'Edit'
|
||||||
|
id?: string
|
||||||
|
initialStep?: 'Details' | 'Profiles' | 'Feeds'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE
|
// NOTE
|
||||||
|
|||||||
@@ -41,4 +41,5 @@ export const router = new Router({
|
|||||||
MessagesSettings: '/messages/settings',
|
MessagesSettings: '/messages/settings',
|
||||||
MessagesConversation: '/messages/:conversation',
|
MessagesConversation: '/messages/:conversation',
|
||||||
StarterPackLanding: '/start/:id',
|
StarterPackLanding: '/start/:id',
|
||||||
|
StarterPackWizard: '/starter-pack/create',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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<TStateContext>([
|
||||||
|
{} 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 (
|
||||||
|
<StateContext.Provider value={stateAndReducer}>
|
||||||
|
{children}
|
||||||
|
</StateContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
type Action as WizardAction,
|
||||||
|
type State as WizardState,
|
||||||
|
type Step as WizardStep,
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<View style={[a.px_xl, a.gap_xl, a.mt_md]}>
|
||||||
|
<View style={[{height: 65, marginTop: 20, marginBottom: 50}]}>
|
||||||
|
<StarterPackIcon />
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<TextField.LabelText>{_(msg`Starter pack name`)}</TextField.LabelText>
|
||||||
|
<TextField.Input
|
||||||
|
label={_(
|
||||||
|
msg`${
|
||||||
|
currentProfile?.displayName || currentProfile?.handle
|
||||||
|
}'s starter pack`,
|
||||||
|
)}
|
||||||
|
value={state.name}
|
||||||
|
onChangeText={text => dispatch({type: 'SetName', name: text})}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<TextField.LabelText>{_(msg`Description`)}</TextField.LabelText>
|
||||||
|
<TextField.Root>
|
||||||
|
<TextField.Input
|
||||||
|
label={_(
|
||||||
|
msg`Write a short description of your starter pack. What can new users expect?`,
|
||||||
|
)}
|
||||||
|
value={state.description}
|
||||||
|
onChangeText={text =>
|
||||||
|
dispatch({type: 'SetDescription', description: text})
|
||||||
|
}
|
||||||
|
multiline
|
||||||
|
style={{minHeight: 150}}
|
||||||
|
/>
|
||||||
|
</TextField.Root>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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<GeneratorView>) {
|
||||||
|
return <FeedCard generator={item} />
|
||||||
|
}
|
||||||
|
|
||||||
|
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})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<View style={[a.flex_1, a.justify_center, {marginTop: 100}]}>
|
||||||
|
<View style={[{height: 150, marginBottom: 50}]}>
|
||||||
|
<StarterPackIcon />
|
||||||
|
</View>
|
||||||
|
<View style={[a.gap_lg, a.align_center, a.px_md]}>
|
||||||
|
<Text style={[a.font_bold, a.text_lg, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Starter packs</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.font_bold, a.text_4xl]}>
|
||||||
|
<Trans>Invites, but personal</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.text_center, a.text_md, a.px_md]}>
|
||||||
|
<Trans>
|
||||||
|
Create your own Bluesky starter packs and invite people directly to
|
||||||
|
your favorite feeds, profiles, and more.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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<AppBskyActorDefs.ProfileViewBasic>) => {
|
||||||
|
return (
|
||||||
|
<WizardProfileCard profile={item} state={state} dispatch={dispatch} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View style={[a.flex_1]}>
|
||||||
|
{state.profiles.length > 0 ? (
|
||||||
|
<List
|
||||||
|
data={state.profiles}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={keyExtractor}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ListEmpty />
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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<CommonNavigatorParams, 'StarterPackWizard'>) {
|
||||||
|
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 (
|
||||||
|
<WizardReady
|
||||||
|
mode={mode}
|
||||||
|
id={id}
|
||||||
|
initialStep={initialStep}
|
||||||
|
starterPack={starterPack}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function WizardReady({
|
||||||
|
mode,
|
||||||
|
initialStep,
|
||||||
|
starterPack,
|
||||||
|
}: {
|
||||||
|
mode: 'Create' | 'Edit'
|
||||||
|
id?: string
|
||||||
|
initialStep?: 'Details' | 'Profiles' | 'Feeds'
|
||||||
|
starterPack?: any
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Provider
|
||||||
|
initialState={mode === 'Edit' ? starterPack : undefined}
|
||||||
|
initialStep={initialStep}>
|
||||||
|
<WizardInner />
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function WizardInner() {
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
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<WizardStep, {header: string; button: string}> =
|
||||||
|
{
|
||||||
|
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 (
|
||||||
|
<CenteredView
|
||||||
|
style={[a.flex_1, {marginBottom: bottomOffset + 20}]}
|
||||||
|
sideBorders>
|
||||||
|
<ViewHeader
|
||||||
|
title={uiStrings.header}
|
||||||
|
onBackPress={
|
||||||
|
state.currentStep !== 'Landing'
|
||||||
|
? () => dispatch({type: 'Back'})
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
showBorder={true}
|
||||||
|
showOnDesktop={true}
|
||||||
|
renderButton={
|
||||||
|
state.currentStep === 'Profiles'
|
||||||
|
? () => (
|
||||||
|
<Button
|
||||||
|
label={_(msg`Cancel`)}
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
size="xsmall"
|
||||||
|
onPress={addProfilesControl.open}
|
||||||
|
style={{marginLeft: -15}}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Add</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Container>
|
||||||
|
<StepView />
|
||||||
|
</Container>
|
||||||
|
<KeyboardStickyView offset={{opened: bottomBarOffset}}>
|
||||||
|
<View style={a.px_md}>
|
||||||
|
<Button
|
||||||
|
label={uiStrings.button}
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
onPress={onNext}
|
||||||
|
disabled={!state.canNext || state.processing}>
|
||||||
|
<ButtonText>{uiStrings.button}</ButtonText>
|
||||||
|
{state.processing && (
|
||||||
|
<Loader size="md" style={t.atoms.text_contrast_low} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</KeyboardStickyView>
|
||||||
|
|
||||||
|
<WizardAddProfilesDialog
|
||||||
|
control={addProfilesControl}
|
||||||
|
state={state}
|
||||||
|
dispatch={dispatch}
|
||||||
|
/>
|
||||||
|
</CenteredView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Container({children}: {children: React.ReactNode}) {
|
||||||
|
const [state] = useWizardState()
|
||||||
|
|
||||||
|
if (state.currentStep === 'Profiles' || state.currentStep === 'Feeds') {
|
||||||
|
return <View style={[a.flex_1]}>{children}</View>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardAwareScrollView style={[a.flex_1]}>
|
||||||
|
{children}
|
||||||
|
</KeyboardAwareScrollView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function StepView() {
|
||||||
|
const [state] = useWizardState()
|
||||||
|
|
||||||
|
if (state.currentStep === 'Landing') {
|
||||||
|
return <StepLanding />
|
||||||
|
}
|
||||||
|
if (state.currentStep === 'Details') {
|
||||||
|
return <StepDetails />
|
||||||
|
}
|
||||||
|
if (state.currentStep === 'Profiles') {
|
||||||
|
return <StepProfiles />
|
||||||
|
}
|
||||||
|
if (state.currentStep === 'Feeds') {
|
||||||
|
return <StepFeeds />
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ export function ViewHeader({
|
|||||||
showOnDesktop,
|
showOnDesktop,
|
||||||
showBorder,
|
showBorder,
|
||||||
renderButton,
|
renderButton,
|
||||||
|
onBackPress: onPressBackOverride,
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
@@ -37,6 +38,7 @@ export function ViewHeader({
|
|||||||
showOnDesktop?: boolean
|
showOnDesktop?: boolean
|
||||||
showBorder?: boolean
|
showBorder?: boolean
|
||||||
renderButton?: () => JSX.Element
|
renderButton?: () => JSX.Element
|
||||||
|
onBackPress?: () => void
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
@@ -48,11 +50,15 @@ export function ViewHeader({
|
|||||||
|
|
||||||
const onPressBack = React.useCallback(() => {
|
const onPressBack = React.useCallback(() => {
|
||||||
if (navigation.canGoBack()) {
|
if (navigation.canGoBack()) {
|
||||||
navigation.goBack()
|
if (typeof onPressBackOverride === 'function') {
|
||||||
|
onPressBackOverride()
|
||||||
|
} else {
|
||||||
|
navigation.goBack()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
navigation.navigate('Home')
|
navigation.navigate('Home')
|
||||||
}
|
}
|
||||||
}, [navigation])
|
}, [navigation, onPressBackOverride])
|
||||||
|
|
||||||
const onPressMenu = React.useCallback(() => {
|
const onPressMenu = React.useCallback(() => {
|
||||||
track('ViewHeader:MenuButtonClicked')
|
track('ViewHeader:MenuButtonClicked')
|
||||||
|
|||||||
@@ -876,6 +876,18 @@ export function SettingsScreen({}: Props) {
|
|||||||
<Trans>Delete chat declaration record</Trans>
|
<Trans>Delete chat declaration record</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[pal.view, styles.linkCardNoIcon]}
|
||||||
|
onPress={() =>
|
||||||
|
navigation.navigate('StarterPackWizard', {mode: 'Create'})
|
||||||
|
}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Navigate to Starter Pack Wizard`)}
|
||||||
|
accessibilityHint={_(msg`Navigates to Starter Pack Wizard`)}>
|
||||||
|
<Text type="lg" style={pal.text}>
|
||||||
|
<Trans>Navigate to Starter Pack Wizard</Trans>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[pal.view, styles.linkCardNoIcon]}
|
style={[pal.view, styles.linkCardNoIcon]}
|
||||||
onPress={onPressResetOnboarding}
|
onPress={onPressResetOnboarding}
|
||||||
|
|||||||
Reference in New Issue
Block a user