Onboarding recommended follows (#1457)
* upgrade api package * add RecommendedFollows as a step in onboarding * add list of recommended follows from suggested actor model * remove dead code * hoist suggestedActors into onboarding model * add comments * load more suggested follows on follow * styling changes * add animation * tweak animations * adjust styling slightly * adjust styles on mobile * styling improvements for web * fix text alignment in RecommendedFollows * dedupe inserted suggestions * fix animation duration * Minor spacing tweak --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com> and Eric Bailey <git@esb.lol>zio/stable
parent
da8499c881
commit
859588c3f6
|
@ -25,7 +25,7 @@
|
|||
"build:apk": "eas build -p android --profile dev-android-apk"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.6.12",
|
||||
"@atproto/api": "^0.6.13",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
|
|
|
@ -2,10 +2,12 @@ import {makeAutoObservable} from 'mobx'
|
|||
import {RootStoreModel} from '../root-store'
|
||||
import {hasProp} from 'lib/type-guards'
|
||||
import {track} from 'lib/analytics/analytics'
|
||||
import {SuggestedActorsModel} from './suggested-actors'
|
||||
|
||||
export const OnboardingScreenSteps = {
|
||||
Welcome: 'Welcome',
|
||||
RecommendedFeeds: 'RecommendedFeeds',
|
||||
RecommendedFollows: 'RecommendedFollows',
|
||||
Home: 'Home',
|
||||
} as const
|
||||
|
||||
|
@ -16,7 +18,11 @@ export class OnboardingModel {
|
|||
// state
|
||||
step: OnboardingStep = 'Home' // default state to skip onboarding, only enabled for new users by calling start()
|
||||
|
||||
// data
|
||||
suggestedActors: SuggestedActorsModel
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
this.suggestedActors = new SuggestedActorsModel(this.rootStore)
|
||||
makeAutoObservable(this, {
|
||||
rootStore: false,
|
||||
hydrate: false,
|
||||
|
@ -56,6 +62,11 @@ export class OnboardingModel {
|
|||
this.step = 'RecommendedFeeds'
|
||||
return this.step
|
||||
} else if (this.step === 'RecommendedFeeds') {
|
||||
this.step = 'RecommendedFollows'
|
||||
// prefetch recommended follows
|
||||
this.suggestedActors.loadMore(true)
|
||||
return this.step
|
||||
} else if (this.step === 'RecommendedFollows') {
|
||||
this.finish()
|
||||
return this.step
|
||||
} else {
|
||||
|
|
|
@ -19,6 +19,7 @@ export class SuggestedActorsModel {
|
|||
loadMoreCursor: string | undefined = undefined
|
||||
error = ''
|
||||
hasMore = false
|
||||
lastInsertedAtIndex = -1
|
||||
|
||||
// data
|
||||
suggestions: SuggestedActor[] = []
|
||||
|
@ -110,6 +111,24 @@ export class SuggestedActorsModel {
|
|||
}
|
||||
})
|
||||
|
||||
async insertSuggestionsByActor(actor: string, indexToInsertAt: number) {
|
||||
// fetch suggestions
|
||||
const res =
|
||||
await this.rootStore.agent.app.bsky.graph.getSuggestedFollowsByActor({
|
||||
actor: actor,
|
||||
})
|
||||
const {suggestions: moreSuggestions} = res.data
|
||||
this.rootStore.me.follows.hydrateProfiles(moreSuggestions)
|
||||
// dedupe
|
||||
const toInsert = moreSuggestions.filter(
|
||||
s => !this.suggestions.find(s2 => s2.did === s.did),
|
||||
)
|
||||
// insert
|
||||
this.suggestions.splice(indexToInsertAt + 1, 0, ...toInsert)
|
||||
// update index
|
||||
this.lastInsertedAtIndex = indexToInsertAt
|
||||
}
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||
import {useStores} from 'state/index'
|
||||
import {Welcome} from './onboarding/Welcome'
|
||||
import {RecommendedFeeds} from './onboarding/RecommendedFeeds'
|
||||
import {RecommendedFollows} from './onboarding/RecommendedFollows'
|
||||
|
||||
export const Onboarding = observer(function OnboardingImpl() {
|
||||
const pal = usePalette('default')
|
||||
|
@ -28,6 +29,9 @@ export const Onboarding = observer(function OnboardingImpl() {
|
|||
{store.onboarding.step === 'RecommendedFeeds' && (
|
||||
<RecommendedFeeds next={next} />
|
||||
)}
|
||||
{store.onboarding.step === 'RecommendedFollows' && (
|
||||
<RecommendedFollows next={next} />
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</SafeAreaView>
|
||||
)
|
||||
|
|
|
@ -96,7 +96,7 @@ export const RecommendedFeeds = observer(function RecommendedFeedsImpl({
|
|||
<Text
|
||||
type="2xl-medium"
|
||||
style={{color: '#fff', position: 'relative', top: -1}}>
|
||||
Done
|
||||
Next
|
||||
</Text>
|
||||
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
|
||||
</View>
|
||||
|
@ -215,6 +215,7 @@ const mStyles = StyleSheet.create({
|
|||
marginBottom: 16,
|
||||
marginHorizontal: 16,
|
||||
marginTop: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonText: {
|
||||
textAlign: 'center',
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
import React from 'react'
|
||||
import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {TabletOrDesktop, Mobile} from 'view/com/util/layouts/Breakpoints'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||
import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useStores} from 'state/index'
|
||||
import {RecommendedFollowsItem} from './RecommendedFollowsItem'
|
||||
|
||||
type Props = {
|
||||
next: () => void
|
||||
}
|
||||
export const RecommendedFollows = observer(function RecommendedFollowsImpl({
|
||||
next,
|
||||
}: Props) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const {isTabletOrMobile} = useWebMediaQueries()
|
||||
|
||||
React.useEffect(() => {
|
||||
// Load suggested actors if not already loaded
|
||||
// prefetch should happen in the onboarding model
|
||||
if (
|
||||
!store.onboarding.suggestedActors.hasLoaded ||
|
||||
store.onboarding.suggestedActors.isEmpty
|
||||
) {
|
||||
store.onboarding.suggestedActors.loadMore(true)
|
||||
}
|
||||
}, [store])
|
||||
|
||||
const title = (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
pal.textLight,
|
||||
tdStyles.title1,
|
||||
isTabletOrMobile && tdStyles.title1Small,
|
||||
]}>
|
||||
Follow some
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
tdStyles.title2,
|
||||
isTabletOrMobile && tdStyles.title2Small,
|
||||
]}>
|
||||
Recommended
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
tdStyles.title2,
|
||||
isTabletOrMobile && tdStyles.title2Small,
|
||||
]}>
|
||||
Users
|
||||
</Text>
|
||||
<Text type="2xl-medium" style={[pal.textLight, tdStyles.description]}>
|
||||
Follow some users to get started. We can recommend you more users based
|
||||
on who you find interesting.
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: 20,
|
||||
}}>
|
||||
<Button onPress={next} testID="continueBtn">
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 2,
|
||||
gap: 6,
|
||||
}}>
|
||||
<Text
|
||||
type="2xl-medium"
|
||||
style={{color: '#fff', position: 'relative', top: -1}}>
|
||||
Done
|
||||
</Text>
|
||||
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
|
||||
</View>
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabletOrDesktop>
|
||||
<TitleColumnLayout
|
||||
testID="recommendedFollowsOnboarding"
|
||||
title={title}
|
||||
horizontal
|
||||
titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}}
|
||||
contentStyle={{paddingHorizontal: 0}}>
|
||||
{store.onboarding.suggestedActors.isLoading ? (
|
||||
<ActivityIndicator size="large" />
|
||||
) : (
|
||||
<FlatList
|
||||
data={store.onboarding.suggestedActors.suggestions}
|
||||
renderItem={({item, index}) => (
|
||||
<RecommendedFollowsItem item={item} index={index} />
|
||||
)}
|
||||
keyExtractor={(item, index) => item.did + index.toString()}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
)}
|
||||
</TitleColumnLayout>
|
||||
</TabletOrDesktop>
|
||||
|
||||
<Mobile>
|
||||
<View style={[mStyles.container]} testID="recommendedFollowsOnboarding">
|
||||
<View>
|
||||
<ViewHeader
|
||||
title="Recommended Follows"
|
||||
showBackButton={false}
|
||||
showOnDesktop
|
||||
/>
|
||||
<Text type="lg-medium" style={[pal.text, mStyles.header]}>
|
||||
Check out some recommended users. Follow them to see similar
|
||||
users.
|
||||
</Text>
|
||||
</View>
|
||||
{store.onboarding.suggestedActors.isLoading ? (
|
||||
<ActivityIndicator size="large" />
|
||||
) : (
|
||||
<FlatList
|
||||
data={store.onboarding.suggestedActors.suggestions}
|
||||
renderItem={({item, index}) => (
|
||||
<RecommendedFollowsItem item={item} index={index} />
|
||||
)}
|
||||
keyExtractor={(item, index) => item.did + index.toString()}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
onPress={next}
|
||||
label="Continue"
|
||||
testID="continueBtn"
|
||||
style={mStyles.button}
|
||||
labelStyle={mStyles.buttonText}
|
||||
/>
|
||||
</View>
|
||||
</Mobile>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
const tdStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
marginHorizontal: 16,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
title1: {
|
||||
fontSize: 36,
|
||||
fontWeight: '800',
|
||||
textAlign: 'right',
|
||||
},
|
||||
title1Small: {
|
||||
fontSize: 24,
|
||||
},
|
||||
title2: {
|
||||
fontSize: 58,
|
||||
fontWeight: '800',
|
||||
textAlign: 'right',
|
||||
},
|
||||
title2Small: {
|
||||
fontSize: 36,
|
||||
},
|
||||
description: {
|
||||
maxWidth: 400,
|
||||
marginTop: 10,
|
||||
marginLeft: 'auto',
|
||||
textAlign: 'right',
|
||||
},
|
||||
})
|
||||
|
||||
const mStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
header: {
|
||||
marginBottom: 16,
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
button: {
|
||||
marginBottom: 16,
|
||||
marginHorizontal: 16,
|
||||
marginTop: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonText: {
|
||||
textAlign: 'center',
|
||||
fontSize: 18,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
})
|
|
@ -0,0 +1,160 @@
|
|||
import React, {useMemo} from 'react'
|
||||
import {View, StyleSheet, ActivityIndicator} from 'react-native'
|
||||
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {useStores} from 'state/index'
|
||||
import {FollowButton} from 'view/com/profile/FollowButton'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {SuggestedActor} from 'state/models/discovery/suggested-actors'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {s} from 'lib/styles'
|
||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import Animated, {FadeInRight} from 'react-native-reanimated'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
|
||||
type Props = {
|
||||
item: SuggestedActor
|
||||
index: number
|
||||
}
|
||||
export const RecommendedFollowsItem: React.FC<Props> = ({item, index}) => {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const delay = useMemo(() => {
|
||||
return (
|
||||
50 *
|
||||
(Math.abs(store.onboarding.suggestedActors.lastInsertedAtIndex - index) %
|
||||
5)
|
||||
)
|
||||
}, [index, store.onboarding.suggestedActors.lastInsertedAtIndex])
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
entering={FadeInRight.delay(delay).springify()}
|
||||
style={[
|
||||
styles.cardContainer,
|
||||
pal.view,
|
||||
pal.border,
|
||||
{
|
||||
maxWidth: isMobile ? undefined : 670,
|
||||
borderRightWidth: isMobile ? undefined : 1,
|
||||
},
|
||||
]}>
|
||||
<ProfileCard key={item.did} profile={item} index={index} />
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
export const ProfileCard = observer(function ProfileCardImpl({
|
||||
profile,
|
||||
index,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
index: number
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const moderation = moderateProfile(profile, store.preferences.moderationOpts)
|
||||
const [addingMoreSuggestions, setAddingMoreSuggestions] =
|
||||
React.useState(false)
|
||||
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
<View style={styles.layout}>
|
||||
<View style={styles.layoutAvi}>
|
||||
<UserAvatar
|
||||
size={40}
|
||||
avatar={profile.avatar}
|
||||
moderation={moderation.avatar}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.layoutContent}>
|
||||
<Text
|
||||
type="2xl-bold"
|
||||
style={[s.bold, pal.text]}
|
||||
numberOfLines={1}
|
||||
lineHeight={1.2}>
|
||||
{sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.profile,
|
||||
)}
|
||||
</Text>
|
||||
<Text type="xl" style={[pal.textLight]} numberOfLines={1}>
|
||||
{sanitizeHandle(profile.handle, '@')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<FollowButton
|
||||
did={profile.did}
|
||||
labelStyle={styles.followButton}
|
||||
onToggleFollow={async isFollow => {
|
||||
if (isFollow) {
|
||||
setAddingMoreSuggestions(true)
|
||||
await store.onboarding.suggestedActors.insertSuggestionsByActor(
|
||||
profile.did,
|
||||
index,
|
||||
)
|
||||
setAddingMoreSuggestions(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{profile.description ? (
|
||||
<View style={styles.details}>
|
||||
<Text type="lg" style={pal.text} numberOfLines={4}>
|
||||
{profile.description as string}
|
||||
</Text>
|
||||
</View>
|
||||
) : undefined}
|
||||
{addingMoreSuggestions ? (
|
||||
<View style={styles.addingMoreContainer}>
|
||||
<ActivityIndicator size="small" color={pal.colors.text} />
|
||||
<Text style={[pal.text]}>Finding similar accounts...</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
cardContainer: {
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
card: {
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
layout: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
layoutAvi: {
|
||||
width: 54,
|
||||
paddingLeft: 4,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
layoutContent: {
|
||||
flex: 1,
|
||||
paddingRight: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
details: {
|
||||
paddingLeft: 54,
|
||||
paddingRight: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
addingMoreContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 54,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 12,
|
||||
gap: 4,
|
||||
},
|
||||
followButton: {
|
||||
fontSize: 16,
|
||||
},
|
||||
})
|
|
@ -88,6 +88,7 @@ export const WelcomeMobile = observer(function WelcomeMobileImpl({
|
|||
onPress={next}
|
||||
label="Continue"
|
||||
testID="continueBtn"
|
||||
style={[styles.buttonContainer]}
|
||||
labelStyle={styles.buttonText}
|
||||
/>
|
||||
</View>
|
||||
|
@ -117,6 +118,9 @@ const styles = StyleSheet.create({
|
|||
spacer: {
|
||||
height: 20,
|
||||
},
|
||||
buttonContainer: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonText: {
|
||||
textAlign: 'center',
|
||||
fontSize: 18,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {StyleProp, TextStyle, View} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {Button, ButtonType} from '../util/forms/Button'
|
||||
import {useStores} from 'state/index'
|
||||
|
@ -11,11 +11,13 @@ export const FollowButton = observer(function FollowButtonImpl({
|
|||
followedType = 'default',
|
||||
did,
|
||||
onToggleFollow,
|
||||
labelStyle,
|
||||
}: {
|
||||
unfollowedType?: ButtonType
|
||||
followedType?: ButtonType
|
||||
did: string
|
||||
onToggleFollow?: (v: boolean) => void
|
||||
labelStyle?: StyleProp<TextStyle>
|
||||
}) {
|
||||
const store = useStores()
|
||||
const followState = store.me.follows.getFollowState(did)
|
||||
|
@ -28,18 +30,18 @@ export const FollowButton = observer(function FollowButtonImpl({
|
|||
const updatedFollowState = await store.me.follows.fetchFollowState(did)
|
||||
if (updatedFollowState === FollowState.Following) {
|
||||
try {
|
||||
onToggleFollow?.(false)
|
||||
await store.agent.deleteFollow(store.me.follows.getFollowUri(did))
|
||||
store.me.follows.removeFollow(did)
|
||||
onToggleFollow?.(false)
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed to delete follow', e)
|
||||
Toast.show('An issue occurred, please try again.')
|
||||
}
|
||||
} else if (updatedFollowState === FollowState.NotFollowing) {
|
||||
try {
|
||||
onToggleFollow?.(true)
|
||||
const res = await store.agent.follow(did)
|
||||
store.me.follows.addFollow(did, res.uri)
|
||||
onToggleFollow?.(true)
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed to create follow', e)
|
||||
Toast.show('An issue occurred, please try again.')
|
||||
|
@ -52,8 +54,10 @@ export const FollowButton = observer(function FollowButtonImpl({
|
|||
type={
|
||||
followState === FollowState.Following ? followedType : unfollowedType
|
||||
}
|
||||
labelStyle={labelStyle}
|
||||
onPress={onToggleFollowInner}
|
||||
label={followState === FollowState.Following ? 'Unfollow' : 'Follow'}
|
||||
withLoading={true}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -7,6 +7,8 @@ import {
|
|||
Pressable,
|
||||
ViewStyle,
|
||||
PressableStateCallbackType,
|
||||
ActivityIndicator,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {Text} from '../text/Text'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
|
@ -48,17 +50,19 @@ export function Button({
|
|||
accessibilityHint,
|
||||
accessibilityLabelledBy,
|
||||
onAccessibilityEscape,
|
||||
withLoading = false,
|
||||
}: React.PropsWithChildren<{
|
||||
type?: ButtonType
|
||||
label?: string
|
||||
style?: StyleProp<ViewStyle>
|
||||
labelStyle?: StyleProp<TextStyle>
|
||||
onPress?: () => void
|
||||
onPress?: () => void | Promise<void>
|
||||
testID?: string
|
||||
accessibilityLabel?: string
|
||||
accessibilityHint?: string
|
||||
accessibilityLabelledBy?: string
|
||||
onAccessibilityEscape?: () => void
|
||||
withLoading?: boolean
|
||||
}>) {
|
||||
const theme = useTheme()
|
||||
const typeOuterStyle = choose<ViewStyle, Record<ButtonType, ViewStyle>>(
|
||||
|
@ -138,13 +142,16 @@ export function Button({
|
|||
},
|
||||
)
|
||||
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
const onPressWrapped = React.useCallback(
|
||||
(event: Event) => {
|
||||
async (event: Event) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
onPress?.()
|
||||
withLoading && setIsLoading(true)
|
||||
await onPress?.()
|
||||
withLoading && setIsLoading(false)
|
||||
},
|
||||
[onPress],
|
||||
[onPress, withLoading],
|
||||
)
|
||||
|
||||
const getStyle = React.useCallback(
|
||||
|
@ -160,23 +167,35 @@ export function Button({
|
|||
[typeOuterStyle, style],
|
||||
)
|
||||
|
||||
const renderChildern = React.useCallback(() => {
|
||||
if (!label) {
|
||||
return children
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.labelContainer}>
|
||||
{label && withLoading && isLoading ? (
|
||||
<ActivityIndicator size={12} color={typeLabelStyle.color} />
|
||||
) : null}
|
||||
<Text type="button" style={[typeLabelStyle, labelStyle]}>
|
||||
{label}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}, [children, label, withLoading, isLoading, typeLabelStyle, labelStyle])
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={getStyle}
|
||||
onPress={onPressWrapped}
|
||||
disabled={isLoading}
|
||||
testID={testID}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
accessibilityHint={accessibilityHint}
|
||||
accessibilityLabelledBy={accessibilityLabelledBy}
|
||||
onAccessibilityEscape={onAccessibilityEscape}>
|
||||
{label ? (
|
||||
<Text type="button" style={[typeLabelStyle, labelStyle]}>
|
||||
{label}
|
||||
</Text>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
{renderChildern}
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
@ -187,4 +206,8 @@ const styles = StyleSheet.create({
|
|||
paddingVertical: 8,
|
||||
borderRadius: 24,
|
||||
},
|
||||
labelContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -34,10 +34,10 @@
|
|||
jsonpointer "^5.0.0"
|
||||
leven "^3.1.0"
|
||||
|
||||
"@atproto/api@^0.6.12":
|
||||
version "0.6.12"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.12.tgz#f39ad9d225aafc5fd90f07d0011d63435c657775"
|
||||
integrity sha512-9R8F78553GI47Iq4FDVwL05LorWTQZQ6FmFsDF/+yryiA+a/VVyvYG4USSptURBZCRgZA5VgTW1We/PwAcDfEA==
|
||||
"@atproto/api@^0.6.13":
|
||||
version "0.6.13"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.13.tgz#26caeae983c577dfedf6ab1f054b31eb158d8ca6"
|
||||
integrity sha512-MudGswKKFJmeh4RoN9LZnYoHib0L7QEIzOdkRU26Fr0dBqSQrnWwLYA9zsNjNg/mZKDyweYkP1ChTNkQvNiYFw==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.2.0"
|
||||
"@atproto/lexicon" "^0.2.0"
|
||||
|
|
Loading…
Reference in New Issue