[Reduced Onboarding] Add profile step (#3933)
* Onboarding avatar creator or upload (#2860) * add screen to onboarding flow * update base * add icon * fix icon * fix after merge * create flatlist * add emoji list * add state context, pressables * select/update * add camera icon * add photo selection button * image selection * cleanup * add most needed icons * fix icon naming * add icons * export path strings for emoji * canvas drawing for web * types * move breakpoints to individual steps * create canvas * canvas working 🎉 * update state * it works! * working on both platforms * remove comments * remove log * remove unused web canvas * animate picture selection/removal * compress images on web correctly * add times icon * scrollable horizontal flatlist on web * prefetch * adjustments * add more assets * remove unused smiles * add all the icons * adjust color options * animate grow/shrink selections * change layout on tablet/desktop * better web layout * fix path * adjust web layout * organize * organize imports and cleanup styles * make generated images smaller * implement design changes use row for buttons on web use RNGH FlatList random color at start improve logic update dialog for web update dialog style on mobile some more progress create dialog simplify context start implementing design * rm change * cleanup imports * trigger a pr label * Formatting --------- Co-authored-by: Eric Bailey <git@esb.lol> (cherry picked from commit 087186e3867b0eefb11a056b0b644f5585fa16bd) * UI tweaks * Revert layout change * Gate avi upload * Support returning to profile step * Add Statsig --------- Co-authored-by: Hailey <me@haileyok.com> Co-authored-by: Dan Abramov <dan.abramov@gmail.com>zio/stable
parent
80ce6f980e
commit
4e37e2f59b
|
@ -185,6 +185,7 @@
|
||||||
"react-native-uitextview": "^1.1.6",
|
"react-native-uitextview": "^1.1.6",
|
||||||
"react-native-url-polyfill": "^1.3.0",
|
"react-native-url-polyfill": "^1.3.0",
|
||||||
"react-native-uuid": "^2.0.1",
|
"react-native-uuid": "^2.0.1",
|
||||||
|
"react-native-view-shot": "^3.8.0",
|
||||||
"react-native-web": "~0.19.6",
|
"react-native-web": "~0.19.6",
|
||||||
"react-native-web-webview": "^1.0.2",
|
"react-native-web-webview": "^1.0.2",
|
||||||
"react-native-webview": "13.6.4",
|
"react-native-webview": "13.6.4",
|
||||||
|
|
|
@ -213,7 +213,8 @@ export function Inner({children, style}: DialogInnerProps) {
|
||||||
return (
|
return (
|
||||||
<BottomSheetView
|
<BottomSheetView
|
||||||
style={[
|
style={[
|
||||||
a.p_xl,
|
a.py_xl,
|
||||||
|
a.px_xl,
|
||||||
{
|
{
|
||||||
paddingTop: 40,
|
paddingTop: 40,
|
||||||
borderTopLeftRadius: 40,
|
borderTopLeftRadius: 40,
|
||||||
|
|
|
@ -150,6 +150,8 @@ export type TrackPropertiesMap = {
|
||||||
}
|
}
|
||||||
'OnboardingV2:StepModeration:Start': {}
|
'OnboardingV2:StepModeration:Start': {}
|
||||||
'OnboardingV2:StepModeration:End': {}
|
'OnboardingV2:StepModeration:End': {}
|
||||||
|
'OnboardingV2:StepProfile:Start': {}
|
||||||
|
'OnboardingV2:StepProfile:End': {}
|
||||||
'OnboardingV2:StepFinished:Start': {}
|
'OnboardingV2:StepFinished:Start': {}
|
||||||
'OnboardingV2:StepFinished:End': {}
|
'OnboardingV2:StepFinished:End': {}
|
||||||
'OnboardingV2:Complete': {}
|
'OnboardingV2:Complete': {}
|
||||||
|
|
|
@ -48,6 +48,7 @@ export type LogEvents = {
|
||||||
selectedFeedsLength: number
|
selectedFeedsLength: number
|
||||||
}
|
}
|
||||||
'onboarding:moderation:nextPressed': {}
|
'onboarding:moderation:nextPressed': {}
|
||||||
|
'onboarding:profile:nextPressed': {}
|
||||||
'onboarding:finished:nextPressed': {}
|
'onboarding:finished:nextPressed': {}
|
||||||
'home:feedDisplayed': {
|
'home:feedDisplayed': {
|
||||||
feedUrl: string
|
feedUrl: string
|
||||||
|
|
|
@ -23,7 +23,7 @@ import {createPortalGroup} from '#/components/Portal'
|
||||||
import {leading, P, Text} from '#/components/Typography'
|
import {leading, P, Text} from '#/components/Typography'
|
||||||
import {IS_DEV} from '#/env'
|
import {IS_DEV} from '#/env'
|
||||||
|
|
||||||
const COL_WIDTH = 500
|
const COL_WIDTH = 420
|
||||||
|
|
||||||
export const OnboardingControls = createPortalGroup()
|
export const OnboardingControls = createPortalGroup()
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {logger} from '#/logger'
|
||||||
import {useOverwriteSavedFeedsMutation} from '#/state/queries/preferences'
|
import {useOverwriteSavedFeedsMutation} from '#/state/queries/preferences'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {useOnboardingDispatch} from '#/state/shell'
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
|
import {uploadBlob} from 'lib/api'
|
||||||
import {
|
import {
|
||||||
DescriptionText,
|
DescriptionText,
|
||||||
OnboardingControls,
|
OnboardingControls,
|
||||||
|
@ -46,11 +47,13 @@ export function StepFinished() {
|
||||||
const finishOnboarding = React.useCallback(async () => {
|
const finishOnboarding = React.useCallback(async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
|
|
||||||
|
// TODO uncomment
|
||||||
const {
|
const {
|
||||||
interestsStepResults,
|
interestsStepResults,
|
||||||
suggestedAccountsStepResults,
|
suggestedAccountsStepResults,
|
||||||
algoFeedsStepResults,
|
algoFeedsStepResults,
|
||||||
topicalFeedsStepResults,
|
topicalFeedsStepResults,
|
||||||
|
profileStepResults,
|
||||||
} = state
|
} = state
|
||||||
const {selectedInterests} = interestsStepResults
|
const {selectedInterests} = interestsStepResults
|
||||||
const selectedFeeds = [
|
const selectedFeeds = [
|
||||||
|
@ -110,6 +113,26 @@ export function StepFinished() {
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (gate('reduced_onboarding_and_home_algo')) {
|
||||||
|
await getAgent().upsertProfile(async existing => {
|
||||||
|
existing = existing ?? {}
|
||||||
|
|
||||||
|
if (profileStepResults.imageUri && profileStepResults.imageMime) {
|
||||||
|
const res = await uploadBlob(
|
||||||
|
getAgent(),
|
||||||
|
profileStepResults.imageUri,
|
||||||
|
profileStepResults.imageMime,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res.data.blob) {
|
||||||
|
existing.avatar = res.data.blob
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing
|
||||||
|
})
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.info(`onboarding: bulk save failed`)
|
logger.info(`onboarding: bulk save failed`)
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
|
|
|
@ -31,8 +31,8 @@ import {Text} from '#/components/Typography'
|
||||||
export function StepInterests() {
|
export function StepInterests() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {track} = useAnalytics()
|
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const {track} = useAnalytics()
|
||||||
const {state, dispatch, interestsDisplayNames} = React.useContext(Context)
|
const {state, dispatch, interestsDisplayNames} = React.useContext(Context)
|
||||||
const [saving, setSaving] = React.useState(false)
|
const [saving, setSaving] = React.useState(false)
|
||||||
const [interests, setInterests] = React.useState<string[]>(
|
const [interests, setInterests] = React.useState<string[]>(
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {Image as ExpoImage} from 'expo-image'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {AvatarCreatorCircle} from '#/screens/Onboarding/StepProfile/AvatarCreatorCircle'
|
||||||
|
import {useAvatar} from '#/screens/Onboarding/StepProfile/index'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
|
import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil'
|
||||||
|
import {StreamingLive_Stroke2_Corner0_Rounded as StreamingLive} from '#/components/icons/StreamingLive'
|
||||||
|
|
||||||
|
export function AvatarCircle({
|
||||||
|
openLibrary,
|
||||||
|
openCreator,
|
||||||
|
}: {
|
||||||
|
openLibrary: () => unknown
|
||||||
|
openCreator: () => unknown
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
const {avatar} = useAvatar()
|
||||||
|
|
||||||
|
const styles = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
imageContainer: [
|
||||||
|
a.rounded_full,
|
||||||
|
a.overflow_hidden,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
{
|
||||||
|
height: 200,
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[t.atoms.bg_contrast_25, t.atoms.border_contrast_low],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{avatar.useCreatedAvatar ? (
|
||||||
|
<AvatarCreatorCircle avatar={avatar} size={200} />
|
||||||
|
) : avatar.image ? (
|
||||||
|
<ExpoImage
|
||||||
|
source={avatar.image.path}
|
||||||
|
style={styles.imageContainer}
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
transition={{duration: 300, effect: 'cross-dissolve'}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View style={styles.imageContainer}>
|
||||||
|
<StreamingLive
|
||||||
|
height={100}
|
||||||
|
width={100}
|
||||||
|
style={{color: t.palette.contrast_200}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View style={[a.absolute, {bottom: 2, right: 2}]}>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Select an avatar`)}
|
||||||
|
size="large"
|
||||||
|
shape="round"
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
onPress={avatar.useCreatedAvatar ? openCreator : openLibrary}>
|
||||||
|
<ButtonIcon icon={Pencil} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
|
||||||
|
import {Avatar} from '#/screens/Onboarding/StepProfile/index'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
|
||||||
|
export function AvatarCreatorCircle({
|
||||||
|
avatar,
|
||||||
|
size = 125,
|
||||||
|
}: {
|
||||||
|
avatar: Avatar
|
||||||
|
size?: number
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const Icon = avatar.placeholder.component
|
||||||
|
|
||||||
|
const styles = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
imageContainer: [
|
||||||
|
a.rounded_full,
|
||||||
|
a.overflow_hidden,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_high,
|
||||||
|
{
|
||||||
|
height: size,
|
||||||
|
width: size,
|
||||||
|
backgroundColor: avatar.backgroundColor,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[avatar.backgroundColor, size, t.atoms.border_contrast_high],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<View style={styles.imageContainer}>
|
||||||
|
<Icon height={85} width={85} style={{color: t.palette.white}} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {Avatar} from '#/screens/Onboarding/StepProfile/index'
|
||||||
|
import {
|
||||||
|
AvatarColor,
|
||||||
|
avatarColors,
|
||||||
|
emojiItems,
|
||||||
|
EmojiName,
|
||||||
|
emojiNames,
|
||||||
|
} from '#/screens/Onboarding/StepProfile/types'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
const ACTIVE_BORDER_WIDTH = 3
|
||||||
|
const ACTIVE_BORDER_STYLES = {
|
||||||
|
top: -ACTIVE_BORDER_WIDTH,
|
||||||
|
bottom: -ACTIVE_BORDER_WIDTH,
|
||||||
|
left: -ACTIVE_BORDER_WIDTH,
|
||||||
|
right: -ACTIVE_BORDER_WIDTH,
|
||||||
|
opacity: 0.5,
|
||||||
|
borderWidth: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AvatarCreatorItems({
|
||||||
|
type,
|
||||||
|
avatar,
|
||||||
|
setAvatar,
|
||||||
|
}: {
|
||||||
|
type: 'emojis' | 'colors'
|
||||||
|
avatar: Avatar
|
||||||
|
setAvatar: React.Dispatch<React.SetStateAction<Avatar>>
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
const isEmojis = type === 'emojis'
|
||||||
|
|
||||||
|
const onSelectEmoji = React.useCallback(
|
||||||
|
(emoji: EmojiName) => {
|
||||||
|
setAvatar(prev => ({
|
||||||
|
...prev,
|
||||||
|
placeholder: emojiItems[emoji],
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
[setAvatar],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onSelectColor = React.useCallback(
|
||||||
|
(color: AvatarColor) => {
|
||||||
|
setAvatar(prev => ({
|
||||||
|
...prev,
|
||||||
|
backgroundColor: color,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
[setAvatar],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.w_full]}>
|
||||||
|
<Text style={[a.pb_md, t.atoms.text_contrast_medium]}>
|
||||||
|
{isEmojis ? (
|
||||||
|
<Trans>Select an emoji</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Select a color</Trans>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_start,
|
||||||
|
a.justify_start,
|
||||||
|
a.flex_wrap,
|
||||||
|
a.gap_md,
|
||||||
|
]}>
|
||||||
|
{isEmojis
|
||||||
|
? emojiNames.map(emojiName => (
|
||||||
|
<Button
|
||||||
|
key={emojiName}
|
||||||
|
label={_(msg`Select the ${emojiName} emoji as your avatar`)}
|
||||||
|
size="small"
|
||||||
|
shape="round"
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
onPress={() => onSelectEmoji(emojiName)}>
|
||||||
|
<ButtonIcon icon={emojiItems[emojiName].component} />
|
||||||
|
{avatar.placeholder.name === emojiName && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.rounded_full,
|
||||||
|
ACTIVE_BORDER_STYLES,
|
||||||
|
{
|
||||||
|
borderColor: avatar.backgroundColor,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
))
|
||||||
|
: avatarColors.map(color => (
|
||||||
|
<Button
|
||||||
|
key={color}
|
||||||
|
label={_(msg`Choose this color as your avatar`)}
|
||||||
|
size="small"
|
||||||
|
shape="round"
|
||||||
|
variant="solid"
|
||||||
|
onPress={() => onSelectColor(color)}>
|
||||||
|
{ctx => (
|
||||||
|
<>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.inset_0,
|
||||||
|
a.rounded_full,
|
||||||
|
{
|
||||||
|
opacity: ctx.hovered || ctx.pressed ? 0.8 : 1,
|
||||||
|
backgroundColor: color,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{avatar.backgroundColor === color && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.rounded_full,
|
||||||
|
ACTIVE_BORDER_STYLES,
|
||||||
|
{
|
||||||
|
borderColor: color,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import ViewShot from 'react-native-view-shot'
|
||||||
|
|
||||||
|
import {useAvatar} from '#/screens/Onboarding/StepProfile/index'
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
|
||||||
|
const SIZE_MULTIPLIER = 1.5
|
||||||
|
|
||||||
|
export interface PlaceholderCanvasRef {
|
||||||
|
capture: () => Promise<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
// This component is supposed to be invisible to the user. We only need this for ViewShot to have something to
|
||||||
|
// "screenshot".
|
||||||
|
export const PlaceholderCanvas = React.forwardRef<PlaceholderCanvasRef, {}>(
|
||||||
|
function PlaceholderCanvas({}, ref) {
|
||||||
|
const {avatar} = useAvatar()
|
||||||
|
const viewshotRef = React.useRef()
|
||||||
|
const Icon = avatar.placeholder.component
|
||||||
|
|
||||||
|
const styles = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
container: [a.absolute, {top: -2000}],
|
||||||
|
imageContainer: [
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
{height: 150 * SIZE_MULTIPLIER, width: 150 * SIZE_MULTIPLIER},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => ({
|
||||||
|
// @ts-ignore this library doesn't have types
|
||||||
|
capture: viewshotRef.current.capture,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<ViewShot
|
||||||
|
// @ts-ignore this library doesn't have types
|
||||||
|
ref={viewshotRef}
|
||||||
|
options={{
|
||||||
|
fileName: 'placeholderAvatar',
|
||||||
|
format: 'jpg',
|
||||||
|
quality: 0.8,
|
||||||
|
height: 150 * SIZE_MULTIPLIER,
|
||||||
|
width: 150 * SIZE_MULTIPLIER,
|
||||||
|
}}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.imageContainer,
|
||||||
|
{backgroundColor: avatar.backgroundColor},
|
||||||
|
]}
|
||||||
|
collapsable={false}>
|
||||||
|
<Icon
|
||||||
|
height={85 * SIZE_MULTIPLIER}
|
||||||
|
width={85 * SIZE_MULTIPLIER}
|
||||||
|
style={{color: 'white'}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</ViewShot>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
|
@ -1,32 +1,210 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
import {Image as ExpoImage} from 'expo-image'
|
||||||
|
import {
|
||||||
|
ImagePickerOptions,
|
||||||
|
launchImageLibraryAsync,
|
||||||
|
MediaTypeOptions,
|
||||||
|
} from 'expo-image-picker'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
|
import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions'
|
||||||
|
import {compressIfNeeded} from 'lib/media/manip'
|
||||||
|
import {openCropper} from 'lib/media/picker'
|
||||||
|
import {getDataUriSize} from 'lib/media/util'
|
||||||
|
import {isNative, isWeb} from 'platform/detection'
|
||||||
import {
|
import {
|
||||||
DescriptionText,
|
DescriptionText,
|
||||||
OnboardingControls,
|
OnboardingControls,
|
||||||
TitleText,
|
TitleText,
|
||||||
} from '#/screens/Onboarding/Layout'
|
} from '#/screens/Onboarding/Layout'
|
||||||
import {Context} from '#/screens/Onboarding/state'
|
import {Context} from '#/screens/Onboarding/state'
|
||||||
import {atoms as a} from '#/alf'
|
import {AvatarCircle} from '#/screens/Onboarding/StepProfile/AvatarCircle'
|
||||||
|
import {AvatarCreatorCircle} from '#/screens/Onboarding/StepProfile/AvatarCreatorCircle'
|
||||||
|
import {AvatarCreatorItems} from '#/screens/Onboarding/StepProfile/AvatarCreatorItems'
|
||||||
|
import {
|
||||||
|
PlaceholderCanvas,
|
||||||
|
PlaceholderCanvasRef,
|
||||||
|
} from '#/screens/Onboarding/StepProfile/PlaceholderCanvas'
|
||||||
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {IconCircle} from '#/components/IconCircle'
|
import {IconCircle} from '#/components/IconCircle'
|
||||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
|
import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo'
|
||||||
import {StreamingLive_Stroke2_Corner0_Rounded as StreamingLive} from '#/components/icons/StreamingLive'
|
import {StreamingLive_Stroke2_Corner0_Rounded as StreamingLive} from '#/components/icons/StreamingLive'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {AvatarColor, avatarColors, Emoji, emojiItems} from './types'
|
||||||
|
|
||||||
|
export interface Avatar {
|
||||||
|
image?: {
|
||||||
|
path: string
|
||||||
|
mime: string
|
||||||
|
size: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
backgroundColor: AvatarColor
|
||||||
|
placeholder: Emoji
|
||||||
|
useCreatedAvatar: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAvatarContext {
|
||||||
|
avatar: Avatar
|
||||||
|
setAvatar: React.Dispatch<React.SetStateAction<Avatar>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const AvatarContext = React.createContext<IAvatarContext>({} as IAvatarContext)
|
||||||
|
export const useAvatar = () => React.useContext(AvatarContext)
|
||||||
|
|
||||||
|
const randomColor =
|
||||||
|
avatarColors[Math.floor(Math.random() * avatarColors.length)]
|
||||||
|
|
||||||
export function StepProfile() {
|
export function StepProfile() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {dispatch} = React.useContext(Context)
|
const t = useTheme()
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const {track} = useAnalytics()
|
||||||
|
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||||
|
const creatorControl = Dialog.useDialogControl()
|
||||||
|
const [error, setError] = React.useState('')
|
||||||
|
|
||||||
|
const {state, dispatch} = React.useContext(Context)
|
||||||
|
const [avatar, setAvatar] = React.useState<Avatar>({
|
||||||
|
image: state.profileStepResults?.image,
|
||||||
|
placeholder: emojiItems.at,
|
||||||
|
backgroundColor: randomColor,
|
||||||
|
useCreatedAvatar: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const canvasRef = React.useRef<PlaceholderCanvasRef>(null)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
track('OnboardingV2:StepProfile:Start')
|
||||||
|
}, [track])
|
||||||
|
|
||||||
|
const openPicker = React.useCallback(
|
||||||
|
async (opts?: ImagePickerOptions) => {
|
||||||
|
const response = await launchImageLibraryAsync({
|
||||||
|
exif: false,
|
||||||
|
mediaTypes: MediaTypeOptions.Images,
|
||||||
|
quality: 1,
|
||||||
|
...opts,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (response.assets ?? [])
|
||||||
|
.slice(0, 1)
|
||||||
|
.filter(asset => {
|
||||||
|
if (
|
||||||
|
!asset.mimeType?.startsWith('image/') ||
|
||||||
|
(!asset.mimeType?.endsWith('jpeg') &&
|
||||||
|
!asset.mimeType?.endsWith('jpg') &&
|
||||||
|
!asset.mimeType?.endsWith('png'))
|
||||||
|
) {
|
||||||
|
setError(_(msg`Only .jpg and .png files are supported`))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.map(image => ({
|
||||||
|
mime: 'image/jpeg',
|
||||||
|
height: image.height,
|
||||||
|
width: image.width,
|
||||||
|
path: image.uri,
|
||||||
|
size: getDataUriSize(image.uri),
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
[_, setError],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onContinue = React.useCallback(async () => {
|
||||||
|
let imageUri = avatar?.image?.path
|
||||||
|
if (!imageUri || avatar.useCreatedAvatar) {
|
||||||
|
imageUri = await canvasRef.current?.capture()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageUri) {
|
||||||
|
dispatch({
|
||||||
|
type: 'setProfileStepResults',
|
||||||
|
image: avatar.image,
|
||||||
|
imageUri,
|
||||||
|
imageMime: avatar.image?.mime ?? 'image/jpeg',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const onContinue = React.useCallback(() => {
|
|
||||||
dispatch({type: 'next'})
|
dispatch({type: 'next'})
|
||||||
}, [dispatch])
|
track('OnboardingV2:StepProfile:End')
|
||||||
|
logEvent('onboarding:profile:nextPressed', {})
|
||||||
|
}, [avatar.image, avatar.useCreatedAvatar, dispatch, track])
|
||||||
|
|
||||||
|
const onDoneCreating = React.useCallback(() => {
|
||||||
|
setAvatar(prev => ({
|
||||||
|
...prev,
|
||||||
|
useCreatedAvatar: true,
|
||||||
|
}))
|
||||||
|
creatorControl.close()
|
||||||
|
}, [creatorControl])
|
||||||
|
|
||||||
|
const openLibrary = React.useCallback(async () => {
|
||||||
|
if (!(await requestPhotoAccessIfNeeded())) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setError('')
|
||||||
|
|
||||||
|
const items = await openPicker({
|
||||||
|
aspect: [1, 1],
|
||||||
|
})
|
||||||
|
let image = items[0]
|
||||||
|
if (!image) return
|
||||||
|
|
||||||
|
if (!isWeb) {
|
||||||
|
image = await openCropper({
|
||||||
|
mediaType: 'photo',
|
||||||
|
cropperCircleOverlay: true,
|
||||||
|
height: image.height,
|
||||||
|
width: image.width,
|
||||||
|
path: image.path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
image = await compressIfNeeded(image, 1000000)
|
||||||
|
|
||||||
|
// If we are on mobile, prefetching the image will load the image into memory before we try and display it,
|
||||||
|
// stopping any brief flickers.
|
||||||
|
if (isNative) {
|
||||||
|
await ExpoImage.prefetch(image.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
setAvatar(prev => ({
|
||||||
|
...prev,
|
||||||
|
image,
|
||||||
|
useCreatedAvatar: false,
|
||||||
|
}))
|
||||||
|
}, [requestPhotoAccessIfNeeded, setAvatar, openPicker, setError])
|
||||||
|
|
||||||
|
const onSecondaryPress = React.useCallback(() => {
|
||||||
|
if (avatar.useCreatedAvatar) {
|
||||||
|
openLibrary()
|
||||||
|
} else {
|
||||||
|
creatorControl.open()
|
||||||
|
}
|
||||||
|
}, [avatar.useCreatedAvatar, creatorControl, openLibrary])
|
||||||
|
|
||||||
|
const value = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
avatar,
|
||||||
|
setAvatar,
|
||||||
|
}),
|
||||||
|
[avatar],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.align_start]}>
|
<AvatarContext.Provider value={value}>
|
||||||
|
<View style={[a.align_start, t.atoms.bg, a.justify_between]}>
|
||||||
<IconCircle icon={StreamingLive} style={[a.mb_2xl]} />
|
<IconCircle icon={StreamingLive} style={[a.mb_2xl]} />
|
||||||
|
|
||||||
<TitleText>
|
<TitleText>
|
||||||
<Trans>Give your profile a face</Trans>
|
<Trans>Give your profile a face</Trans>
|
||||||
</TitleText>
|
</TitleText>
|
||||||
|
@ -36,8 +214,35 @@ export function StepProfile() {
|
||||||
an avatar.
|
an avatar.
|
||||||
</Trans>
|
</Trans>
|
||||||
</DescriptionText>
|
</DescriptionText>
|
||||||
|
<View
|
||||||
|
style={[a.w_full, a.align_center, {paddingTop: gtMobile ? 80 : 40}]}>
|
||||||
|
<AvatarCircle
|
||||||
|
openLibrary={openLibrary}
|
||||||
|
openCreator={creatorControl.open}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.gap_sm,
|
||||||
|
a.align_center,
|
||||||
|
a.mt_xl,
|
||||||
|
a.py_md,
|
||||||
|
a.px_lg,
|
||||||
|
a.border,
|
||||||
|
a.rounded_md,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<CircleInfo_Stroke2_Corner0_Rounded size="sm" />
|
||||||
|
<Text style={[a.leading_snug]}>{error}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
<OnboardingControls.Portal>
|
<OnboardingControls.Portal>
|
||||||
|
<View style={[a.gap_md, gtMobile && {flexDirection: 'row-reverse'}]}>
|
||||||
<Button
|
<Button
|
||||||
variant="gradient"
|
variant="gradient"
|
||||||
color="gradient_sky"
|
color="gradient_sky"
|
||||||
|
@ -49,7 +254,66 @@ export function StepProfile() {
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
<ButtonIcon icon={ChevronRight} position="right" />
|
<ButtonIcon icon={ChevronRight} position="right" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
label={_(msg`Open avatar creator`)}
|
||||||
|
onPress={onSecondaryPress}>
|
||||||
|
<ButtonText>
|
||||||
|
{avatar.useCreatedAvatar ? (
|
||||||
|
<Trans>Upload a photo instead</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Create an avatar instead</Trans>
|
||||||
|
)}
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
</OnboardingControls.Portal>
|
</OnboardingControls.Portal>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<Dialog.Outer control={creatorControl}>
|
||||||
|
<Dialog.Handle />
|
||||||
|
<Dialog.Inner
|
||||||
|
label="Avatar creator"
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
width: 'auto',
|
||||||
|
maxWidth: 410,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View style={[a.align_center, {paddingTop: 20}]}>
|
||||||
|
<AvatarCreatorCircle avatar={avatar} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.pt_3xl, a.gap_lg]}>
|
||||||
|
<AvatarCreatorItems
|
||||||
|
type="emojis"
|
||||||
|
avatar={avatar}
|
||||||
|
setAvatar={setAvatar}
|
||||||
|
/>
|
||||||
|
<AvatarCreatorItems
|
||||||
|
type="colors"
|
||||||
|
avatar={avatar}
|
||||||
|
setAvatar={setAvatar}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={[a.pt_4xl]}>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
label={_(msg`Done`)}
|
||||||
|
onPress={onDoneCreating}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Done</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</Dialog.Inner>
|
||||||
|
</Dialog.Outer>
|
||||||
|
|
||||||
|
<PlaceholderCanvas ref={canvasRef} />
|
||||||
|
</AvatarContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
import {Alien_Stroke2_Corner0_Rounded as Alien} from '#/components/icons/Alien'
|
||||||
|
import {Apple_Stroke2_Corner0_Rounded as Apple} from '#/components/icons/Apple'
|
||||||
|
import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
|
||||||
|
import {Atom_Stroke2_Corner0_Rounded as Atom} from '#/components/icons/Atom'
|
||||||
|
import {Celebrate_Stroke2_Corner0_Rounded as Celebrate} from '#/components/icons/Celebrate'
|
||||||
|
import {Coffee_Stroke2_Corner0_Rounded as Coffee} from '#/components/icons/Coffee'
|
||||||
|
import {
|
||||||
|
EmojiArc_Stroke2_Corner0_Rounded as EmojiArc,
|
||||||
|
EmojiHeartEyes_Stroke2_Corner0_Rounded as EmojiHeartEyes,
|
||||||
|
} from '#/components/icons/Emoji'
|
||||||
|
import {Explosion_Stroke2_Corner0_Rounded as Explosion} from '#/components/icons/Explosion'
|
||||||
|
import {GameController_Stroke2_Corner0_Rounded as GameController} from '#/components/icons/GameController'
|
||||||
|
import {Lab_Stroke2_Corner0_Rounded as Lab} from '#/components/icons/Lab'
|
||||||
|
import {Leaf_Stroke2_Corner0_Rounded as Leaf} from '#/components/icons/Leaf'
|
||||||
|
import {MusicNote_Stroke2_Corner0_Rounded as MusicNote} from '#/components/icons/MusicNote'
|
||||||
|
import {PiggyBank_Stroke2_Corner0_Rounded as PiggyBank} from '#/components/icons/PiggyBank'
|
||||||
|
import {Pizza_Stroke2_Corner0_Rounded as Pizza} from '#/components/icons/Pizza'
|
||||||
|
import {Poop_Stroke2_Corner0_Rounded as Poop} from '#/components/icons/Poop'
|
||||||
|
import {Rose_Stroke2_Corner0_Rounded as Rose} from '#/components/icons/Rose'
|
||||||
|
import {Shaka_Stroke2_Corner0_Rounded as Shaka} from '#/components/icons/Shaka'
|
||||||
|
import {UFO_Stroke2_Corner0_Rounded as UFO} from '#/components/icons/UFO'
|
||||||
|
import {Zap_Stroke2_Corner0_Rounded as Zap} from '#/components/icons/Zap'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If you want to add or remove icons from the selection, just add the name to the `emojiNames` array and
|
||||||
|
* add the item to the `emojiItems` record..
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const emojiNames = [
|
||||||
|
'at',
|
||||||
|
'arc',
|
||||||
|
'heartEyes',
|
||||||
|
'alien',
|
||||||
|
'apple',
|
||||||
|
'atom',
|
||||||
|
'celebrate',
|
||||||
|
'coffee',
|
||||||
|
'gameController',
|
||||||
|
'leaf',
|
||||||
|
'musicNote',
|
||||||
|
'pizza',
|
||||||
|
'rose',
|
||||||
|
'shaka',
|
||||||
|
'ufo',
|
||||||
|
'zap',
|
||||||
|
'explosion',
|
||||||
|
'lab',
|
||||||
|
'piggyBank',
|
||||||
|
'poop',
|
||||||
|
] as const
|
||||||
|
export type EmojiName = (typeof emojiNames)[number]
|
||||||
|
|
||||||
|
export interface Emoji {
|
||||||
|
name: EmojiName
|
||||||
|
component: typeof EmojiArc
|
||||||
|
}
|
||||||
|
export const emojiItems: Record<EmojiName, Emoji> = {
|
||||||
|
at: {
|
||||||
|
name: 'at',
|
||||||
|
component: At,
|
||||||
|
},
|
||||||
|
arc: {
|
||||||
|
name: 'arc',
|
||||||
|
component: EmojiArc,
|
||||||
|
},
|
||||||
|
heartEyes: {
|
||||||
|
name: 'heartEyes',
|
||||||
|
component: EmojiHeartEyes,
|
||||||
|
},
|
||||||
|
alien: {
|
||||||
|
name: 'alien',
|
||||||
|
component: Alien,
|
||||||
|
},
|
||||||
|
apple: {
|
||||||
|
name: 'apple',
|
||||||
|
component: Apple,
|
||||||
|
},
|
||||||
|
atom: {
|
||||||
|
name: 'atom',
|
||||||
|
component: Atom,
|
||||||
|
},
|
||||||
|
celebrate: {
|
||||||
|
name: 'celebrate',
|
||||||
|
component: Celebrate,
|
||||||
|
},
|
||||||
|
coffee: {
|
||||||
|
name: 'coffee',
|
||||||
|
component: Coffee,
|
||||||
|
},
|
||||||
|
gameController: {
|
||||||
|
name: 'gameController',
|
||||||
|
component: GameController,
|
||||||
|
},
|
||||||
|
leaf: {
|
||||||
|
name: 'leaf',
|
||||||
|
component: Leaf,
|
||||||
|
},
|
||||||
|
musicNote: {
|
||||||
|
name: 'musicNote',
|
||||||
|
component: MusicNote,
|
||||||
|
},
|
||||||
|
pizza: {
|
||||||
|
name: 'pizza',
|
||||||
|
component: Pizza,
|
||||||
|
},
|
||||||
|
rose: {
|
||||||
|
name: 'rose',
|
||||||
|
component: Rose,
|
||||||
|
},
|
||||||
|
shaka: {
|
||||||
|
name: 'shaka',
|
||||||
|
component: Shaka,
|
||||||
|
},
|
||||||
|
ufo: {
|
||||||
|
name: 'ufo',
|
||||||
|
component: UFO,
|
||||||
|
},
|
||||||
|
zap: {
|
||||||
|
name: 'zap',
|
||||||
|
component: Zap,
|
||||||
|
},
|
||||||
|
explosion: {
|
||||||
|
name: 'explosion',
|
||||||
|
component: Explosion,
|
||||||
|
},
|
||||||
|
lab: {
|
||||||
|
name: 'lab',
|
||||||
|
component: Lab,
|
||||||
|
},
|
||||||
|
piggyBank: {
|
||||||
|
name: 'piggyBank',
|
||||||
|
component: PiggyBank,
|
||||||
|
},
|
||||||
|
poop: {
|
||||||
|
name: 'poop',
|
||||||
|
component: Poop,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const avatarColors = [
|
||||||
|
'#FE8311',
|
||||||
|
'#FED811',
|
||||||
|
'#73DF84',
|
||||||
|
'#1185FE',
|
||||||
|
'#EF75EA',
|
||||||
|
'#F55454',
|
||||||
|
] as const
|
||||||
|
export type AvatarColor = (typeof avatarColors)[number]
|
|
@ -69,9 +69,9 @@ export function Inner({
|
||||||
|
|
||||||
export function StepSuggestedAccounts() {
|
export function StepSuggestedAccounts() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const {state, dispatch, interestsDisplayNames} = React.useContext(Context)
|
const {state, dispatch, interestsDisplayNames} = React.useContext(Context)
|
||||||
const {gtMobile} = useBreakpoints()
|
|
||||||
const suggestedDids = React.useMemo(() => {
|
const suggestedDids = React.useMemo(() => {
|
||||||
return aggregateInterestItems(
|
return aggregateInterestItems(
|
||||||
state.interestsStepResults.selectedInterests,
|
state.interestsStepResults.selectedInterests,
|
||||||
|
|
|
@ -13,6 +13,7 @@ export type OnboardingState = {
|
||||||
| 'algoFeeds'
|
| 'algoFeeds'
|
||||||
| 'topicalFeeds'
|
| 'topicalFeeds'
|
||||||
| 'moderation'
|
| 'moderation'
|
||||||
|
| 'profile'
|
||||||
| 'finished'
|
| 'finished'
|
||||||
activeStepIndex: number
|
activeStepIndex: number
|
||||||
|
|
||||||
|
@ -30,6 +31,13 @@ export type OnboardingState = {
|
||||||
feedUris: string[]
|
feedUris: string[]
|
||||||
}
|
}
|
||||||
profileStepResults: {
|
profileStepResults: {
|
||||||
|
image?: {
|
||||||
|
path: string
|
||||||
|
mime: string
|
||||||
|
size: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
imageUri?: string
|
imageUri?: string
|
||||||
imageMime?: string
|
imageMime?: string
|
||||||
}
|
}
|
||||||
|
@ -64,6 +72,7 @@ export type OnboardingAction =
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'setProfileStepResults'
|
type: 'setProfileStepResults'
|
||||||
|
image?: OnboardingState['profileStepResults']['image']
|
||||||
imageUri: string
|
imageUri: string
|
||||||
imageMime: string
|
imageMime: string
|
||||||
}
|
}
|
||||||
|
@ -80,7 +89,7 @@ export type ApiResponseMap = {
|
||||||
|
|
||||||
export const initialState: OnboardingState = {
|
export const initialState: OnboardingState = {
|
||||||
hasPrev: false,
|
hasPrev: false,
|
||||||
totalSteps: 7,
|
totalSteps: 8,
|
||||||
activeStep: 'interests',
|
activeStep: 'interests',
|
||||||
activeStepIndex: 1,
|
activeStepIndex: 1,
|
||||||
|
|
||||||
|
@ -102,6 +111,7 @@ export const initialState: OnboardingState = {
|
||||||
feedUris: [],
|
feedUris: [],
|
||||||
},
|
},
|
||||||
profileStepResults: {
|
profileStepResults: {
|
||||||
|
image: undefined,
|
||||||
imageUri: '',
|
imageUri: '',
|
||||||
imageMime: '',
|
imageMime: '',
|
||||||
},
|
},
|
||||||
|
@ -168,8 +178,11 @@ export function reducer(
|
||||||
next.activeStep = 'moderation'
|
next.activeStep = 'moderation'
|
||||||
next.activeStepIndex = 6
|
next.activeStepIndex = 6
|
||||||
} else if (s.activeStep === 'moderation') {
|
} else if (s.activeStep === 'moderation') {
|
||||||
next.activeStep = 'finished'
|
next.activeStep = 'profile'
|
||||||
next.activeStepIndex = 7
|
next.activeStepIndex = 7
|
||||||
|
} else if (s.activeStep === 'profile') {
|
||||||
|
next.activeStep = 'finished'
|
||||||
|
next.activeStepIndex = 8
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -189,9 +202,12 @@ export function reducer(
|
||||||
} else if (s.activeStep === 'moderation') {
|
} else if (s.activeStep === 'moderation') {
|
||||||
next.activeStep = 'topicalFeeds'
|
next.activeStep = 'topicalFeeds'
|
||||||
next.activeStepIndex = 5
|
next.activeStepIndex = 5
|
||||||
} else if (s.activeStep === 'finished') {
|
} else if (s.activeStep === 'profile') {
|
||||||
next.activeStep = 'moderation'
|
next.activeStep = 'moderation'
|
||||||
next.activeStepIndex = 6
|
next.activeStepIndex = 6
|
||||||
|
} else if (s.activeStep === 'finished') {
|
||||||
|
next.activeStep = 'profile'
|
||||||
|
next.activeStepIndex = 7
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -226,6 +242,14 @@ export function reducer(
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 'setProfileStepResults': {
|
||||||
|
next.profileStepResults = {
|
||||||
|
image: a.image,
|
||||||
|
imageUri: a.imageUri,
|
||||||
|
imageMime: a.imageMime,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
|
@ -243,6 +267,7 @@ export function reducer(
|
||||||
suggestedAccountsStepResults: state.suggestedAccountsStepResults,
|
suggestedAccountsStepResults: state.suggestedAccountsStepResults,
|
||||||
algoFeedsStepResults: state.algoFeedsStepResults,
|
algoFeedsStepResults: state.algoFeedsStepResults,
|
||||||
topicalFeedsStepResults: state.topicalFeedsStepResults,
|
topicalFeedsStepResults: state.topicalFeedsStepResults,
|
||||||
|
profileStepResults: state.profileStepResults,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (s.activeStep !== state.activeStep) {
|
if (s.activeStep !== state.activeStep) {
|
||||||
|
@ -276,6 +301,7 @@ export const initialStateReduced: OnboardingState = {
|
||||||
feedUris: [],
|
feedUris: [],
|
||||||
},
|
},
|
||||||
profileStepResults: {
|
profileStepResults: {
|
||||||
|
image: undefined,
|
||||||
imageUri: '',
|
imageUri: '',
|
||||||
imageMime: '',
|
imageMime: '',
|
||||||
},
|
},
|
||||||
|
@ -330,6 +356,7 @@ export function reducerReduced(
|
||||||
}
|
}
|
||||||
case 'setProfileStepResults': {
|
case 'setProfileStepResults': {
|
||||||
next.profileStepResults = {
|
next.profileStepResults = {
|
||||||
|
image: a.image,
|
||||||
imageUri: a.imageUri,
|
imageUri: a.imageUri,
|
||||||
imageMime: a.imageMime,
|
imageMime: a.imageMime,
|
||||||
}
|
}
|
||||||
|
|
41
yarn.lock
41
yarn.lock
|
@ -9114,6 +9114,11 @@ base-64@0.1.0, base-64@^0.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
|
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
|
||||||
integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==
|
integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==
|
||||||
|
|
||||||
|
base64-arraybuffer@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
|
||||||
|
integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
|
||||||
|
|
||||||
base64-js@^1.0.2, base64-js@^1.2.3, base64-js@^1.3.1, base64-js@^1.5.1:
|
base64-js@^1.0.2, base64-js@^1.2.3, base64-js@^1.3.1, base64-js@^1.5.1:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||||
|
@ -10239,6 +10244,13 @@ css-in-js-utils@^3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
hyphenate-style-name "^1.0.3"
|
hyphenate-style-name "^1.0.3"
|
||||||
|
|
||||||
|
css-line-break@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
|
||||||
|
integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
|
||||||
|
dependencies:
|
||||||
|
utrie "^1.0.2"
|
||||||
|
|
||||||
css-loader@^6.5.1:
|
css-loader@^6.5.1:
|
||||||
version "6.8.1"
|
version "6.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.8.1.tgz#0f8f52699f60f5e679eab4ec0fcd68b8e8a50a88"
|
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.8.1.tgz#0f8f52699f60f5e679eab4ec0fcd68b8e8a50a88"
|
||||||
|
@ -13233,6 +13245,14 @@ html-webpack-plugin@^5.5.0:
|
||||||
pretty-error "^4.0.0"
|
pretty-error "^4.0.0"
|
||||||
tapable "^2.0.0"
|
tapable "^2.0.0"
|
||||||
|
|
||||||
|
html2canvas@^1.4.1:
|
||||||
|
version "1.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
|
||||||
|
integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
|
||||||
|
dependencies:
|
||||||
|
css-line-break "^2.1.0"
|
||||||
|
text-segmentation "^1.0.3"
|
||||||
|
|
||||||
htmlparser2@^6.1.0:
|
htmlparser2@^6.1.0:
|
||||||
version "6.1.0"
|
version "6.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
|
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
|
||||||
|
@ -18827,6 +18847,13 @@ react-native-uuid@^2.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/react-native-uuid/-/react-native-uuid-2.0.1.tgz#ed4e2dfb1683eddb66967eb5dca140dfe1abddb9"
|
resolved "https://registry.yarnpkg.com/react-native-uuid/-/react-native-uuid-2.0.1.tgz#ed4e2dfb1683eddb66967eb5dca140dfe1abddb9"
|
||||||
integrity sha512-cptnoIbL53GTCrWlb/+jrDC6tvb7ypIyzbXNJcpR3Vab0mkeaaVd5qnB3f0whXYzS+SMoSQLcUUB0gEWqkPC0g==
|
integrity sha512-cptnoIbL53GTCrWlb/+jrDC6tvb7ypIyzbXNJcpR3Vab0mkeaaVd5qnB3f0whXYzS+SMoSQLcUUB0gEWqkPC0g==
|
||||||
|
|
||||||
|
react-native-view-shot@^3.8.0:
|
||||||
|
version "3.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-view-shot/-/react-native-view-shot-3.8.0.tgz#1aa1905f0e79428ca32bf80c16fd4abc719c600b"
|
||||||
|
integrity sha512-4cU8SOhMn3YQIrskh+5Q8VvVRxQOu8/s1M9NAL4z5BY1Rm0HXMWkQJ4N0XsZ42+Yca+y86ISF3LC5qdLPvPuiA==
|
||||||
|
dependencies:
|
||||||
|
html2canvas "^1.4.1"
|
||||||
|
|
||||||
react-native-web-webview@^1.0.2:
|
react-native-web-webview@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-web-webview/-/react-native-web-webview-1.0.2.tgz#c215efa70c17589f2c8d640b1f1dc669b18c6e02"
|
resolved "https://registry.yarnpkg.com/react-native-web-webview/-/react-native-web-webview-1.0.2.tgz#c215efa70c17589f2c8d640b1f1dc669b18c6e02"
|
||||||
|
@ -20831,6 +20858,13 @@ test-exclude@^6.0.0:
|
||||||
glob "^7.1.4"
|
glob "^7.1.4"
|
||||||
minimatch "^3.0.4"
|
minimatch "^3.0.4"
|
||||||
|
|
||||||
|
text-segmentation@^1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943"
|
||||||
|
integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
|
||||||
|
dependencies:
|
||||||
|
utrie "^1.0.2"
|
||||||
|
|
||||||
text-table@^0.2.0:
|
text-table@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||||
|
@ -21486,6 +21520,13 @@ utils-merge@1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||||
|
|
||||||
|
utrie@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"
|
||||||
|
integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
|
||||||
|
dependencies:
|
||||||
|
base64-arraybuffer "^1.0.2"
|
||||||
|
|
||||||
uuid@^3.0.1, uuid@^3.3.2:
|
uuid@^3.0.1, uuid@^3.3.2:
|
||||||
version "3.4.0"
|
version "3.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||||
|
|
Loading…
Reference in New Issue