get suggested accounts working on native
This commit is contained in:
@@ -90,6 +90,9 @@ export type MetricEvents = {
|
|||||||
selectedInterests: string[]
|
selectedInterests: string[]
|
||||||
selectedInterestsLength: number
|
selectedInterestsLength: number
|
||||||
}
|
}
|
||||||
|
'onboarding:suggestedAccounts:tabPressed': {
|
||||||
|
tab: string
|
||||||
|
}
|
||||||
'onboarding:suggestedAccounts:nextPressed': {
|
'onboarding:suggestedAccounts:nextPressed': {
|
||||||
selectedAccountsLength: number
|
selectedAccountsLength: number
|
||||||
skipped: boolean
|
skipped: boolean
|
||||||
@@ -249,6 +252,7 @@ export type MetricEvents = {
|
|||||||
| 'InterstitialDiscover'
|
| 'InterstitialDiscover'
|
||||||
| 'InterstitialProfile'
|
| 'InterstitialProfile'
|
||||||
| 'Profile'
|
| 'Profile'
|
||||||
|
| 'Onboarding'
|
||||||
location: 'Card' | 'Profile'
|
location: 'Card' | 'Profile'
|
||||||
recId?: number
|
recId?: number
|
||||||
position: number
|
position: number
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons
|
|||||||
import {createPortalGroup} from '#/components/Portal'
|
import {createPortalGroup} from '#/components/Portal'
|
||||||
import {P, Text} from '#/components/Typography'
|
import {P, Text} from '#/components/Typography'
|
||||||
|
|
||||||
const COL_WIDTH = 420
|
const ONBOARDING_COL_WIDTH = 420
|
||||||
|
|
||||||
export const OnboardingControls = createPortalGroup()
|
export const OnboardingControls = createPortalGroup()
|
||||||
|
|
||||||
@@ -92,7 +92,8 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
|||||||
top: paddingTop.paddingTop + insets.top - 1,
|
top: paddingTop.paddingTop + insets.top - 1,
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<View style={[a.w_full, a.align_start, {maxWidth: COL_WIDTH}]}>
|
<View
|
||||||
|
style={[a.w_full, a.align_start, {maxWidth: ONBOARDING_COL_WIDTH}]}>
|
||||||
<Button
|
<Button
|
||||||
key={state.activeStep} // remove focus state on nav
|
key={state.activeStep} // remove focus state on nav
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@@ -113,16 +114,11 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
|||||||
style={[a.h_full, a.w_full, {paddingTop: insets.top}]}
|
style={[a.h_full, a.w_full, {paddingTop: insets.top}]}
|
||||||
contentContainerStyle={{borderWidth: 0}}
|
contentContainerStyle={{borderWidth: 0}}
|
||||||
scrollIndicatorInsets={{bottom: footerHeight - insets.bottom}}
|
scrollIndicatorInsets={{bottom: footerHeight - insets.bottom}}
|
||||||
// @ts-ignore web only --prf
|
// @ts-expect-error web only --prf
|
||||||
dataSet={{'stable-gutters': 1}}>
|
dataSet={{'stable-gutters': 1}}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[a.flex_row, a.justify_center, gtMobile ? a.px_5xl : a.px_xl]}>
|
||||||
a.flex_row,
|
<View style={[a.flex_1, {maxWidth: ONBOARDING_COL_WIDTH}]}>
|
||||||
a.justify_center,
|
|
||||||
gtMobile ? a.px_5xl : a.px_xl,
|
|
||||||
a.debug,
|
|
||||||
]}>
|
|
||||||
<View style={[a.flex_1, {maxWidth: COL_WIDTH}]}>
|
|
||||||
<View style={[a.w_full, a.align_center, paddingTop]}>
|
<View style={[a.w_full, a.align_center, paddingTop]}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -166,7 +162,6 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
|||||||
<View
|
<View
|
||||||
onLayout={evt => setFooterHeight(evt.nativeEvent.layout.height)}
|
onLayout={evt => setFooterHeight(evt.nativeEvent.layout.height)}
|
||||||
style={[
|
style={[
|
||||||
// @ts-ignore web only -prf
|
|
||||||
isWeb ? a.fixed : a.absolute,
|
isWeb ? a.fixed : a.absolute,
|
||||||
{bottom: 0, left: 0, right: 0},
|
{bottom: 0, left: 0, right: 0},
|
||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
@@ -184,7 +179,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.w_full,
|
a.w_full,
|
||||||
{maxWidth: COL_WIDTH},
|
{maxWidth: ONBOARDING_COL_WIDTH},
|
||||||
gtMobile && [a.flex_row, a.justify_between, a.align_center],
|
gtMobile && [a.flex_row, a.justify_between, a.align_center],
|
||||||
]}>
|
]}>
|
||||||
{gtMobile &&
|
{gtMobile &&
|
||||||
|
|||||||
@@ -160,7 +160,16 @@ export function StepInterests() {
|
|||||||
|
|
||||||
<View style={[a.w_full, a.pt_2xl]}>
|
<View style={[a.w_full, a.pt_2xl]}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader size="xl" />
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.mt_md,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
{minHeight: 400},
|
||||||
|
]}>
|
||||||
|
<Loader size="xl" />
|
||||||
|
</View>
|
||||||
) : isError || !data ? (
|
) : isError || !data ? (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@@ -1,29 +1,82 @@
|
|||||||
import {useCallback, useContext, useState} from 'react'
|
import {useCallback, useContext, useMemo, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
import {type ModerationOpts} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||||
|
import * as bcp47Match from 'bcp-47-match'
|
||||||
|
|
||||||
|
import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {updateProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
|
import {useLanguagePrefs} from '#/state/preferences'
|
||||||
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import {useOnboardingDispatch} from '#/state/shell'
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
import {OnboardingControls} from '#/screens/Onboarding/Layout'
|
import {OnboardingControls} from '#/screens/Onboarding/Layout'
|
||||||
import {Context} from '#/screens/Onboarding/state'
|
import {
|
||||||
|
Context,
|
||||||
|
popularInterests,
|
||||||
|
useInterestsDisplayNames,
|
||||||
|
} from '#/screens/Onboarding/state'
|
||||||
import {SuggestedAccountsTabBar} from '#/screens/Search/modules/ExploreSuggestedAccounts'
|
import {SuggestedAccountsTabBar} from '#/screens/Search/modules/ExploreSuggestedAccounts'
|
||||||
import {atoms as a, useBreakpoints} from '#/alf'
|
import {useSuggestedUsers} from '#/screens/Search/util/useSuggestedUsers'
|
||||||
|
import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwise} from '#/components/icons/ArrowRotateCounterClockwise'
|
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwiseIcon} from '#/components/icons/ArrowRotateCounterClockwise'
|
||||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
|
import {boostInterests} from '#/components/ProgressGuide/FollowDialog'
|
||||||
|
import {SubtleHover} from '#/components/SubtleHover'
|
||||||
|
import * as toast from '#/components/Toast'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import type * as bsky from '#/types/bsky'
|
||||||
|
import {bulkWriteFollows} from '../util'
|
||||||
|
|
||||||
export function StepSuggestedAccounts() {
|
export function StepSuggestedAccounts() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
|
const agent = useAgent()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const {dispatch} = useContext(Context)
|
const {state, dispatch} = useContext(Context)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const onboardDispatch = useOnboardingDispatch()
|
const onboardDispatch = useOnboardingDispatch()
|
||||||
|
|
||||||
const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
|
const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
|
||||||
|
// keeping track of who was followed via the follow all button
|
||||||
|
// so we can enable/disable the button without having to dig through the shadow cache
|
||||||
|
const [followedUsers, setFollowedUsers] = useState<string[]>([])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Special language handling copied wholesale from the Explore screen
|
||||||
|
*/
|
||||||
|
// FIGURE OUT BEFORE MERGE: IS THIS POPULATED BEFORE FIRST ONBOARDING
|
||||||
|
const {contentLanguages} = useLanguagePrefs()
|
||||||
|
const useFullExperience = useMemo(() => {
|
||||||
|
if (contentLanguages.length === 0) return true
|
||||||
|
return bcp47Match.basicFilter('en', contentLanguages).length > 0
|
||||||
|
}, [contentLanguages])
|
||||||
|
const interestsDisplayNames = useInterestsDisplayNames()
|
||||||
|
const interests = Object.keys(interestsDisplayNames)
|
||||||
|
.sort(boostInterests(popularInterests))
|
||||||
|
.sort(boostInterests(state.interestsStepResults.selectedInterests))
|
||||||
|
const {
|
||||||
|
data: suggestedUsers,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
isRefetching,
|
||||||
|
refetch,
|
||||||
|
} = useSuggestedUsers({
|
||||||
|
category: selectedInterest || (useFullExperience ? null : interests[0]),
|
||||||
|
search: !useFullExperience,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isError = !!error
|
||||||
|
|
||||||
const saveInterests = useCallback(async () => {
|
const saveInterests = useCallback(async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
@@ -42,7 +95,47 @@ export function StepSuggestedAccounts() {
|
|||||||
dispatch({type: 'finish'})
|
dispatch({type: 'finish'})
|
||||||
}, [onboardDispatch, dispatch])
|
}, [onboardDispatch, dispatch])
|
||||||
|
|
||||||
const isError = false
|
const followableDids =
|
||||||
|
suggestedUsers?.actors
|
||||||
|
.filter(
|
||||||
|
user =>
|
||||||
|
user.did !== currentAccount?.did &&
|
||||||
|
!isBlockedOrBlocking(user) &&
|
||||||
|
!isMuted(user) &&
|
||||||
|
!user.viewer?.following &&
|
||||||
|
!followedUsers.includes(user.did),
|
||||||
|
)
|
||||||
|
.map(user => user.did) ?? []
|
||||||
|
|
||||||
|
const {mutate: followAll, isPending: isFollowingAll} = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
for (const did of followableDids) {
|
||||||
|
updateProfileShadow(queryClient, did, {
|
||||||
|
followingUri: 'pending',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const uris = await bulkWriteFollows(agent, followableDids)
|
||||||
|
for (const did of followableDids) {
|
||||||
|
const uri = uris.get(did)
|
||||||
|
updateProfileShadow(queryClient, did, {
|
||||||
|
followingUri: uri,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return followableDids
|
||||||
|
},
|
||||||
|
onSuccess: newlyFollowed => {
|
||||||
|
toast.show(_(msg`Done!`), {type: 'success'})
|
||||||
|
setFollowedUsers(followed => [...followed, ...newlyFollowed])
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.show(
|
||||||
|
_(msg`Failed to follow all suggested accounts, please try again`),
|
||||||
|
{type: 'error'},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const canFollowAll = followableDids.length > 0 && !isFollowingAll
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.align_start]} testID="onboardingInterests">
|
<View style={[a.align_start]} testID="onboardingInterests">
|
||||||
@@ -50,29 +143,76 @@ export function StepSuggestedAccounts() {
|
|||||||
<Trans>Suggested Accounts</Trans>
|
<Trans>Suggested Accounts</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<SuggestedAccountsTabBar
|
<View
|
||||||
selectedInterest={selectedInterest}
|
style={[
|
||||||
onSelectInterest={setSelectedInterest}
|
a.overflow_hidden,
|
||||||
/>
|
a.mt_sm,
|
||||||
|
!gtMobile ? {marginHorizontal: tokens.space.xl * -1} : a.max_w_full,
|
||||||
|
a.flex_1,
|
||||||
|
a.justify_start,
|
||||||
|
]}>
|
||||||
|
<SuggestedAccountsTabBar
|
||||||
|
selectedInterest={selectedInterest}
|
||||||
|
onSelectInterest={setSelectedInterest}
|
||||||
|
priorityInterests={state.interestsStepResults.selectedInterests}
|
||||||
|
leftPadding={gtMobile ? 0 : tokens.space.xl}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isLoading || !moderationOpts ? (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.mt_md,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
{minHeight: 400},
|
||||||
|
]}>
|
||||||
|
<Loader size="xl" />
|
||||||
|
</View>
|
||||||
|
) : isError ? (
|
||||||
|
<View style={[a.flex_1, a.px_xl, a.pt_5xl]}>
|
||||||
|
<Text
|
||||||
|
style={[a.text_center, a.text_md, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>
|
||||||
|
An error occurred while fetching suggested accounts.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.mt_md,
|
||||||
|
a.border_b,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
{suggestedUsers?.actors.map((user, index) => (
|
||||||
|
<SuggestedProfileCard
|
||||||
|
key={user.did}
|
||||||
|
profile={user}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
position={index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
<OnboardingControls.Portal>
|
<OnboardingControls.Portal>
|
||||||
{isError ? (
|
{isError ? (
|
||||||
<View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
|
<View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
|
||||||
<Button
|
<Button
|
||||||
// disabled={isFetching}
|
disabled={isRefetching}
|
||||||
variant="solid"
|
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="large"
|
size="large"
|
||||||
label={_(msg`Retry`)}
|
label={_(msg`Retry`)}
|
||||||
// onPress={() => refetch()}
|
onPress={() => refetch()}>
|
||||||
>
|
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Retry</Trans>
|
<Trans>Retry</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
<ButtonIcon icon={ArrowRotateCounterClockwise} position="right" />
|
<ButtonIcon icon={ArrowRotateCounterClockwiseIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="large"
|
size="large"
|
||||||
label={_(msg`Skip this flow`)}
|
label={_(msg`Skip this flow`)}
|
||||||
@@ -85,17 +225,18 @@ export function StepSuggestedAccounts() {
|
|||||||
) : (
|
) : (
|
||||||
<View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
|
<View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
|
||||||
<Button
|
<Button
|
||||||
disabled={saving}
|
disabled={saving || !canFollowAll}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="large"
|
size="large"
|
||||||
label={_(msg`Follow all accounts`)}>
|
label={_(msg`Follow all accounts`)}
|
||||||
|
onPress={() => followAll()}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Follow all</Trans>
|
<Trans>Follow all</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
|
{isFollowingAll && <ButtonIcon icon={Loader} />}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
variant="solid"
|
|
||||||
color="primary"
|
color="primary"
|
||||||
size="large"
|
size="large"
|
||||||
label={_(msg`Continue to next step`)}
|
label={_(msg`Continue to next step`)}
|
||||||
@@ -103,7 +244,7 @@ export function StepSuggestedAccounts() {
|
|||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Continue</Trans>
|
<Trans>Continue</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
<ButtonIcon icon={saving ? Loader : ChevronRight} />
|
<ButtonIcon icon={saving ? Loader : ChevronRightIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -111,3 +252,79 @@ export function StepSuggestedAccounts() {
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SuggestedProfileCard({
|
||||||
|
profile,
|
||||||
|
moderationOpts,
|
||||||
|
recId,
|
||||||
|
position,
|
||||||
|
}: {
|
||||||
|
profile: bsky.profile.AnyProfileView
|
||||||
|
moderationOpts: ModerationOpts
|
||||||
|
recId?: number
|
||||||
|
position: number
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
return (
|
||||||
|
<ProfileCard.Link
|
||||||
|
profile={profile}
|
||||||
|
onPress={() => {
|
||||||
|
logger.metric(
|
||||||
|
'suggestedUser:press',
|
||||||
|
{
|
||||||
|
logContext: 'Explore',
|
||||||
|
recId,
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{statsig: true},
|
||||||
|
)
|
||||||
|
}}>
|
||||||
|
{s => (
|
||||||
|
<>
|
||||||
|
<SubtleHover hover={s.hovered || s.pressed} />
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.w_full,
|
||||||
|
a.py_lg,
|
||||||
|
a.px_xl,
|
||||||
|
a.border_t,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<ProfileCard.Outer>
|
||||||
|
<ProfileCard.Header>
|
||||||
|
<ProfileCard.Avatar
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
<ProfileCard.NameAndHandle
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
<ProfileCard.FollowButton
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
withIcon={false}
|
||||||
|
logContext="ExploreSuggestedAccounts"
|
||||||
|
onFollow={() => {
|
||||||
|
logger.metric(
|
||||||
|
'suggestedUser:follow',
|
||||||
|
{
|
||||||
|
logContext: 'Explore',
|
||||||
|
location: 'Card',
|
||||||
|
recId,
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{statsig: true},
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ProfileCard.Header>
|
||||||
|
<ProfileCard.Description profile={profile} numberOfLines={2} />
|
||||||
|
</ProfileCard.Outer>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ProfileCard.Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
$Typed,
|
type $Typed,
|
||||||
AppBskyGraphFollow,
|
type AppBskyGraphFollow,
|
||||||
AppBskyGraphGetFollows,
|
type AppBskyGraphGetFollows,
|
||||||
BskyAgent,
|
type BskyAgent,
|
||||||
ComAtprotoRepoApplyWrites,
|
type ComAtprotoRepoApplyWrites,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {TID} from '@atproto/common-web'
|
import {TID} from '@atproto/common-web'
|
||||||
import chunk from 'lodash.chunk'
|
import chunk from 'lodash.chunk'
|
||||||
@@ -42,10 +42,10 @@ export async function bulkWriteFollows(agent: BskyAgent, dids: string[]) {
|
|||||||
}
|
}
|
||||||
await whenFollowsIndexed(agent, session.did, res => !!res.data.follows.length)
|
await whenFollowsIndexed(agent, session.did, res => !!res.data.follows.length)
|
||||||
|
|
||||||
const followUris = new Map()
|
const followUris = new Map<string, string>()
|
||||||
for (const r of followWrites) {
|
for (const r of followWrites) {
|
||||||
followUris.set(
|
followUris.set(
|
||||||
r.value.subject,
|
r.value.subject as string,
|
||||||
`at://${session.did}/app.bsky.graph.follow/${r.rkey}`,
|
`at://${session.did}/app.bsky.graph.follow/${r.rkey}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -751,6 +751,7 @@ export function Explore({
|
|||||||
selectedInterest={selectedInterest}
|
selectedInterest={selectedInterest}
|
||||||
onSelectInterest={setSelectedInterest}
|
onSelectInterest={setSelectedInterest}
|
||||||
hideDefaultTab={item.hideDefaultTab}
|
hideDefaultTab={item.hideDefaultTab}
|
||||||
|
logContext="Explore"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
popularInterests,
|
popularInterests,
|
||||||
useInterestsDisplayNames,
|
useInterestsDisplayNames,
|
||||||
} from '#/screens/Onboarding/state'
|
} from '#/screens/Onboarding/state'
|
||||||
import {useTheme} from '#/alf'
|
import {tokens, useTheme} from '#/alf'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
@@ -55,14 +55,21 @@ export function useLoadEnoughProfiles({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Move to own file -sfn
|
||||||
export function SuggestedAccountsTabBar({
|
export function SuggestedAccountsTabBar({
|
||||||
selectedInterest,
|
selectedInterest,
|
||||||
onSelectInterest,
|
onSelectInterest,
|
||||||
hideDefaultTab,
|
hideDefaultTab,
|
||||||
|
priorityInterests,
|
||||||
|
leftPadding = tokens.space.md,
|
||||||
|
logContext = 'Explore',
|
||||||
}: {
|
}: {
|
||||||
selectedInterest: string | null
|
selectedInterest: string | null
|
||||||
onSelectInterest: (interest: string | null) => void
|
onSelectInterest: (interest: string | null) => void
|
||||||
|
priorityInterests?: string[]
|
||||||
hideDefaultTab?: boolean
|
hideDefaultTab?: boolean
|
||||||
|
leftPadding?: number
|
||||||
|
logContext?: 'Explore' | 'Onboarding'
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const interestsDisplayNames = useInterestsDisplayNames()
|
const interestsDisplayNames = useInterestsDisplayNames()
|
||||||
@@ -71,6 +78,8 @@ export function SuggestedAccountsTabBar({
|
|||||||
const interests = Object.keys(interestsDisplayNames)
|
const interests = Object.keys(interestsDisplayNames)
|
||||||
.sort(boostInterests(popularInterests))
|
.sort(boostInterests(popularInterests))
|
||||||
.sort(boostInterests(personalizedInterests))
|
.sort(boostInterests(personalizedInterests))
|
||||||
|
.sort(boostInterests(priorityInterests))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BlockDrawerGesture>
|
<BlockDrawerGesture>
|
||||||
<Tabs
|
<Tabs
|
||||||
@@ -79,11 +88,19 @@ export function SuggestedAccountsTabBar({
|
|||||||
selectedInterest || (hideDefaultTab ? interests[0] : 'all')
|
selectedInterest || (hideDefaultTab ? interests[0] : 'all')
|
||||||
}
|
}
|
||||||
onSelectTab={tab => {
|
onSelectTab={tab => {
|
||||||
logger.metric(
|
if (logContext === 'Explore') {
|
||||||
'explore:suggestedAccounts:tabPressed',
|
logger.metric(
|
||||||
{tab: tab},
|
'explore:suggestedAccounts:tabPressed',
|
||||||
{statsig: true},
|
{tab: tab},
|
||||||
)
|
{statsig: true},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
logger.metric(
|
||||||
|
'onboarding:suggestedAccounts:tabPressed',
|
||||||
|
{tab: tab},
|
||||||
|
{statsig: true},
|
||||||
|
)
|
||||||
|
}
|
||||||
onSelectInterest(tab === 'all' ? null : tab)
|
onSelectInterest(tab === 'all' ? null : tab)
|
||||||
}}
|
}}
|
||||||
hasSearchText={false}
|
hasSearchText={false}
|
||||||
@@ -99,7 +116,7 @@ export function SuggestedAccountsTabBar({
|
|||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
{
|
{
|
||||||
// visual alignment
|
// visual alignment
|
||||||
paddingLeft: a.px_md.paddingLeft,
|
paddingLeft: leftPadding,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export function useSuggestedUsers({
|
|||||||
isLoading: searched.isLoading,
|
isLoading: searched.isLoading,
|
||||||
error: searched.error,
|
error: searched.error,
|
||||||
isRefetching: searched.isRefetching,
|
isRefetching: searched.isRefetching,
|
||||||
|
refetch: searched.refetch,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
@@ -50,6 +51,7 @@ export function useSuggestedUsers({
|
|||||||
isLoading: curated.isLoading,
|
isLoading: curated.isLoading,
|
||||||
error: curated.error,
|
error: curated.error,
|
||||||
isRefetching: curated.isRefetching,
|
isRefetching: curated.isRefetching,
|
||||||
|
refetch: curated.refetch,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [curated, searched, search])
|
}, [curated, searched, search])
|
||||||
|
|||||||
Reference in New Issue
Block a user