add recId to profile header suggestions
This commit is contained in:
@@ -43,7 +43,7 @@ import {EditProfileDialog} from './EditProfileDialog'
|
|||||||
import {ProfileHeaderHandle} from './Handle'
|
import {ProfileHeaderHandle} from './Handle'
|
||||||
import {ProfileHeaderMetrics} from './Metrics'
|
import {ProfileHeaderMetrics} from './Metrics'
|
||||||
import {ProfileHeaderShell} from './Shell'
|
import {ProfileHeaderShell} from './Shell'
|
||||||
import {AnimatedProfileHeaderSuggestedFollows} from './SuggestedFollows'
|
import {ProfileHeaderSuggestedFollows} from './SuggestedFollows'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
@@ -193,7 +193,7 @@ let ProfileHeaderStandard = ({
|
|||||||
/>
|
/>
|
||||||
</ProfileHeaderShell>
|
</ProfileHeaderShell>
|
||||||
|
|
||||||
<AnimatedProfileHeaderSuggestedFollows
|
<ProfileHeaderSuggestedFollows
|
||||||
isExpanded={showSuggestedFollows}
|
isExpanded={showSuggestedFollows}
|
||||||
actorDid={profile.did}
|
actorDid={profile.did}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react'
|
import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||||
import {type AppBskyActorDefs} from '@atproto/api'
|
|
||||||
|
|
||||||
import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation'
|
import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
@@ -10,198 +9,17 @@ import {
|
|||||||
import {useBreakpoints} from '#/alf'
|
import {useBreakpoints} from '#/alf'
|
||||||
import {ProfileGrid} from '#/components/FeedInterstitials'
|
import {ProfileGrid} from '#/components/FeedInterstitials'
|
||||||
import {IS_ANDROID} from '#/env'
|
import {IS_ANDROID} from '#/env'
|
||||||
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
const DISMISS_ANIMATION_DURATION = 200
|
export function ProfileHeaderSuggestedFollows({
|
||||||
|
|
||||||
export function ProfileHeaderSuggestedFollows({actorDid}: {actorDid: string}) {
|
|
||||||
const {gtMobile} = useBreakpoints()
|
|
||||||
const moderationOpts = useModerationOpts()
|
|
||||||
const maxLength = gtMobile ? 4 : 12
|
|
||||||
const {isLoading, data, error} = useSuggestedFollowsByActorQuery({
|
|
||||||
did: actorDid,
|
|
||||||
})
|
|
||||||
const {
|
|
||||||
data: moreSuggestions,
|
|
||||||
fetchNextPage,
|
|
||||||
hasNextPage,
|
|
||||||
isFetchingNextPage,
|
|
||||||
} = useSuggestedFollowsQuery({limit: 25})
|
|
||||||
|
|
||||||
const [dismissedDids, setDismissedDids] = React.useState<Set<string>>(
|
|
||||||
new Set(),
|
|
||||||
)
|
|
||||||
const [dismissingDids, setDismissingDids] = React.useState<Set<string>>(
|
|
||||||
new Set(),
|
|
||||||
)
|
|
||||||
|
|
||||||
const onDismiss = React.useCallback((did: string) => {
|
|
||||||
// Start the fade animation
|
|
||||||
setDismissingDids(prev => new Set(prev).add(did))
|
|
||||||
// After animation completes, actually remove from list
|
|
||||||
setTimeout(() => {
|
|
||||||
setDismissedDids(prev => new Set(prev).add(did))
|
|
||||||
setDismissingDids(prev => {
|
|
||||||
const next = new Set(prev)
|
|
||||||
next.delete(did)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}, DISMISS_ANIMATION_DURATION)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Combine profiles from the actor-specific query with fallback suggestions
|
|
||||||
const allProfiles = React.useMemo(() => {
|
|
||||||
const actorProfiles = data?.suggestions ?? []
|
|
||||||
const fallbackProfiles =
|
|
||||||
moreSuggestions?.pages.flatMap(page => page.actors) ?? []
|
|
||||||
|
|
||||||
// Dedupe by did, preferring actor-specific profiles
|
|
||||||
const seen = new Set<string>()
|
|
||||||
const combined: AppBskyActorDefs.ProfileView[] = []
|
|
||||||
|
|
||||||
for (const profile of actorProfiles) {
|
|
||||||
if (!seen.has(profile.did)) {
|
|
||||||
seen.add(profile.did)
|
|
||||||
combined.push(profile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const profile of fallbackProfiles) {
|
|
||||||
if (!seen.has(profile.did) && profile.did !== actorDid) {
|
|
||||||
seen.add(profile.did)
|
|
||||||
combined.push(profile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return combined
|
|
||||||
}, [data?.suggestions, moreSuggestions?.pages, actorDid])
|
|
||||||
|
|
||||||
const filteredProfiles = React.useMemo(() => {
|
|
||||||
return allProfiles.filter(p => !dismissedDids.has(p.did))
|
|
||||||
}, [allProfiles, dismissedDids])
|
|
||||||
|
|
||||||
// Fetch more when running low
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (
|
|
||||||
moderationOpts &&
|
|
||||||
filteredProfiles.length < maxLength &&
|
|
||||||
hasNextPage &&
|
|
||||||
!isFetchingNextPage
|
|
||||||
) {
|
|
||||||
fetchNextPage()
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
filteredProfiles.length,
|
|
||||||
maxLength,
|
|
||||||
hasNextPage,
|
|
||||||
isFetchingNextPage,
|
|
||||||
fetchNextPage,
|
|
||||||
moderationOpts,
|
|
||||||
])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProfileGrid
|
|
||||||
isSuggestionsLoading={isLoading}
|
|
||||||
profiles={filteredProfiles}
|
|
||||||
totalProfileCount={allProfiles.length}
|
|
||||||
recId={data?.recId}
|
|
||||||
error={error}
|
|
||||||
viewContext="profileHeader"
|
|
||||||
onDismiss={onDismiss}
|
|
||||||
dismissingDids={dismissingDids}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AnimatedProfileHeaderSuggestedFollows({
|
|
||||||
isExpanded,
|
isExpanded,
|
||||||
actorDid,
|
actorDid,
|
||||||
}: {
|
}: {
|
||||||
isExpanded: boolean
|
isExpanded: boolean
|
||||||
actorDid: string
|
actorDid: string
|
||||||
}) {
|
}) {
|
||||||
const {gtMobile} = useBreakpoints()
|
const {allProfiles, filteredProfiles, onDismiss, isLoading, error} =
|
||||||
const moderationOpts = useModerationOpts()
|
useProfileHeaderSuggestions(actorDid)
|
||||||
const maxLength = gtMobile ? 4 : 12
|
|
||||||
const {isLoading, data, error} = useSuggestedFollowsByActorQuery({
|
|
||||||
did: actorDid,
|
|
||||||
})
|
|
||||||
const {
|
|
||||||
data: moreSuggestions,
|
|
||||||
fetchNextPage,
|
|
||||||
hasNextPage,
|
|
||||||
isFetchingNextPage,
|
|
||||||
} = useSuggestedFollowsQuery({limit: 25})
|
|
||||||
|
|
||||||
const [dismissedDids, setDismissedDids] = React.useState<Set<string>>(
|
|
||||||
new Set(),
|
|
||||||
)
|
|
||||||
const [dismissingDids, setDismissingDids] = React.useState<Set<string>>(
|
|
||||||
new Set(),
|
|
||||||
)
|
|
||||||
|
|
||||||
const onDismiss = React.useCallback((did: string) => {
|
|
||||||
// Start the fade animation
|
|
||||||
setDismissingDids(prev => new Set(prev).add(did))
|
|
||||||
// After animation completes, actually remove from list
|
|
||||||
setTimeout(() => {
|
|
||||||
setDismissedDids(prev => new Set(prev).add(did))
|
|
||||||
setDismissingDids(prev => {
|
|
||||||
const next = new Set(prev)
|
|
||||||
next.delete(did)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}, DISMISS_ANIMATION_DURATION)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Combine profiles from the actor-specific query with fallback suggestions
|
|
||||||
const allProfiles = React.useMemo(() => {
|
|
||||||
const actorProfiles = data?.suggestions ?? []
|
|
||||||
const fallbackProfiles =
|
|
||||||
moreSuggestions?.pages.flatMap(page => page.actors) ?? []
|
|
||||||
|
|
||||||
// Dedupe by did, preferring actor-specific profiles
|
|
||||||
const seen = new Set<string>()
|
|
||||||
const combined: AppBskyActorDefs.ProfileView[] = []
|
|
||||||
|
|
||||||
for (const profile of actorProfiles) {
|
|
||||||
if (!seen.has(profile.did)) {
|
|
||||||
seen.add(profile.did)
|
|
||||||
combined.push(profile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const profile of fallbackProfiles) {
|
|
||||||
if (!seen.has(profile.did) && profile.did !== actorDid) {
|
|
||||||
seen.add(profile.did)
|
|
||||||
combined.push(profile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return combined
|
|
||||||
}, [data?.suggestions, moreSuggestions?.pages, actorDid])
|
|
||||||
|
|
||||||
const filteredProfiles = React.useMemo(() => {
|
|
||||||
return allProfiles.filter(p => !dismissedDids.has(p.did))
|
|
||||||
}, [allProfiles, dismissedDids])
|
|
||||||
|
|
||||||
// Fetch more when running low
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (
|
|
||||||
moderationOpts &&
|
|
||||||
filteredProfiles.length < maxLength &&
|
|
||||||
hasNextPage &&
|
|
||||||
!isFetchingNextPage
|
|
||||||
) {
|
|
||||||
fetchNextPage()
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
filteredProfiles.length,
|
|
||||||
maxLength,
|
|
||||||
hasNextPage,
|
|
||||||
isFetchingNextPage,
|
|
||||||
fetchNextPage,
|
|
||||||
moderationOpts,
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!allProfiles.length && !isLoading) return null
|
if (!allProfiles.length && !isLoading) return null
|
||||||
|
|
||||||
@@ -218,13 +36,92 @@ export function AnimatedProfileHeaderSuggestedFollows({
|
|||||||
isSuggestionsLoading={isLoading}
|
isSuggestionsLoading={isLoading}
|
||||||
profiles={filteredProfiles}
|
profiles={filteredProfiles}
|
||||||
totalProfileCount={allProfiles.length}
|
totalProfileCount={allProfiles.length}
|
||||||
recId={data?.recId}
|
|
||||||
error={error}
|
error={error}
|
||||||
viewContext="profileHeader"
|
viewContext="profileHeader"
|
||||||
onDismiss={onDismiss}
|
onDismiss={onDismiss}
|
||||||
dismissingDids={dismissingDids}
|
|
||||||
isVisible={isExpanded}
|
isVisible={isExpanded}
|
||||||
/>
|
/>
|
||||||
</AccordionAnimation>
|
</AccordionAnimation>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useProfileHeaderSuggestions(actorDid: string) {
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
|
const maxLength = gtMobile ? 4 : 12
|
||||||
|
const {isLoading, data, error} = useSuggestedFollowsByActorQuery({
|
||||||
|
did: actorDid,
|
||||||
|
})
|
||||||
|
const {
|
||||||
|
data: moreSuggestions,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
} = useSuggestedFollowsQuery({limit: 25})
|
||||||
|
|
||||||
|
const [dismissedDids, setDismissedDids] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
|
const onDismiss = useCallback((did: string) => {
|
||||||
|
setDismissedDids(prev => new Set(prev).add(did))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Combine profiles from the actor-specific query with fallback suggestions
|
||||||
|
const allProfiles = useMemo(() => {
|
||||||
|
const actorProfiles = data?.suggestions ?? []
|
||||||
|
const fallbackProfiles =
|
||||||
|
moreSuggestions?.pages.flatMap(page =>
|
||||||
|
page.actors.map(actor => ({actor, recId: page.recId})),
|
||||||
|
) ?? []
|
||||||
|
|
||||||
|
// Dedupe by did, preferring actor-specific profiles
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const combined: {actor: bsky.profile.AnyProfileView; recId?: number}[] = []
|
||||||
|
|
||||||
|
for (const profile of actorProfiles) {
|
||||||
|
if (!seen.has(profile.did)) {
|
||||||
|
seen.add(profile.did)
|
||||||
|
combined.push({actor: profile, recId: data?.recId})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const profile of fallbackProfiles) {
|
||||||
|
if (!seen.has(profile.actor.did) && profile.actor.did !== actorDid) {
|
||||||
|
seen.add(profile.actor.did)
|
||||||
|
combined.push(profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return combined
|
||||||
|
}, [data?.suggestions, moreSuggestions?.pages, actorDid, data?.recId])
|
||||||
|
|
||||||
|
const filteredProfiles = useMemo(() => {
|
||||||
|
return allProfiles.filter(p => !dismissedDids.has(p.actor.did))
|
||||||
|
}, [allProfiles, dismissedDids])
|
||||||
|
|
||||||
|
// Fetch more when running low
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
moderationOpts &&
|
||||||
|
filteredProfiles.length < maxLength &&
|
||||||
|
hasNextPage &&
|
||||||
|
!isFetchingNextPage
|
||||||
|
) {
|
||||||
|
void fetchNextPage()
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
filteredProfiles.length,
|
||||||
|
maxLength,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
moderationOpts,
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
allProfiles,
|
||||||
|
filteredProfiles,
|
||||||
|
onDismiss,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user