add recId to feed interstitials, fix animation on native
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
import React, {useCallback, useEffect, useRef} from 'react'
|
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||||
import {ScrollView, View} from 'react-native'
|
import {ScrollView, View} from 'react-native'
|
||||||
import Animated, {LinearTransition} from 'react-native-reanimated'
|
import Animated, {
|
||||||
|
Easing,
|
||||||
|
FadeIn,
|
||||||
|
FadeOut,
|
||||||
|
LayoutAnimationConfig,
|
||||||
|
LinearTransition,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
import {type AppBskyFeedDefs, AtUri} from '@atproto/api'
|
import {type AppBskyFeedDefs, AtUri} 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'
|
||||||
@@ -21,6 +27,7 @@ import {type SeenPost} from '#/state/userActionHistory'
|
|||||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||||
import {
|
import {
|
||||||
atoms as a,
|
atoms as a,
|
||||||
|
native,
|
||||||
useBreakpoints,
|
useBreakpoints,
|
||||||
useTheme,
|
useTheme,
|
||||||
type ViewStyleProp,
|
type ViewStyleProp,
|
||||||
@@ -152,7 +159,7 @@ function sortSeenPosts(postA: SeenPost, postB: SeenPost): 0 | 1 | -1 {
|
|||||||
function useExperimentalSuggestedUsersQuery() {
|
function useExperimentalSuggestedUsersQuery() {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const userActionSnapshot = userActionHistory.useActionHistorySnapshot()
|
const userActionSnapshot = userActionHistory.useActionHistorySnapshot()
|
||||||
const dids = React.useMemo(() => {
|
const dids = useMemo(() => {
|
||||||
const {likes, follows, followSuggestions, seen} = userActionSnapshot
|
const {likes, follows, followSuggestions, seen} = userActionSnapshot
|
||||||
const likeDids = likes
|
const likeDids = likes
|
||||||
.map(l => new AtUri(l))
|
.map(l => new AtUri(l))
|
||||||
@@ -225,67 +232,54 @@ export function SuggestedFollowsProfile({did}: {did: string}) {
|
|||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
} = useSuggestedFollowsQuery({limit: 25})
|
} = useSuggestedFollowsQuery({limit: 25})
|
||||||
|
|
||||||
const [dismissedDids, setDismissedDids] = React.useState<Set<string>>(
|
const [dismissedDids, setDismissedDids] = useState<Set<string>>(new Set())
|
||||||
new Set(),
|
|
||||||
)
|
|
||||||
const [dismissingDids, setDismissingDids] = React.useState<Set<string>>(
|
|
||||||
new Set(),
|
|
||||||
)
|
|
||||||
|
|
||||||
const onDismiss = React.useCallback((dismissedDid: string) => {
|
const onDismiss = useCallback((dismissedDid: string) => {
|
||||||
// Start the fade animation
|
|
||||||
setDismissingDids(prev => new Set(prev).add(dismissedDid))
|
|
||||||
// After animation completes, actually remove from list
|
|
||||||
setTimeout(() => {
|
|
||||||
setDismissedDids(prev => new Set(prev).add(dismissedDid))
|
setDismissedDids(prev => new Set(prev).add(dismissedDid))
|
||||||
setDismissingDids(prev => {
|
|
||||||
const next = new Set(prev)
|
|
||||||
next.delete(dismissedDid)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}, DISMISS_ANIMATION_DURATION)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Combine profiles from the actor-specific query with fallback suggestions
|
// Combine profiles from the actor-specific query with fallback suggestions
|
||||||
const allProfiles = React.useMemo(() => {
|
const allProfiles = useMemo(() => {
|
||||||
const actorProfiles = data?.suggestions ?? []
|
const actorProfiles = data?.suggestions ?? []
|
||||||
const fallbackProfiles =
|
const fallbackProfiles =
|
||||||
moreSuggestions?.pages.flatMap(page => page.actors) ?? []
|
moreSuggestions?.pages.flatMap(page =>
|
||||||
|
page.actors.map(actor => ({actor, recId: page.recId})),
|
||||||
|
) ?? []
|
||||||
|
|
||||||
// Dedupe by did, preferring actor-specific profiles
|
// Dedupe by did, preferring actor-specific profiles
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
const combined: bsky.profile.AnyProfileView[] = []
|
const combined: {actor: bsky.profile.AnyProfileView; recId?: number}[] = []
|
||||||
|
|
||||||
for (const profile of actorProfiles) {
|
for (const profile of actorProfiles) {
|
||||||
if (!seen.has(profile.did)) {
|
if (!seen.has(profile.did)) {
|
||||||
seen.add(profile.did)
|
seen.add(profile.did)
|
||||||
combined.push(profile)
|
combined.push({actor: profile, recId: data?.recId})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const profile of fallbackProfiles) {
|
for (const profile of fallbackProfiles) {
|
||||||
if (!seen.has(profile.did) && profile.did !== did) {
|
if (!seen.has(profile.actor.did) && profile.actor.did !== did) {
|
||||||
seen.add(profile.did)
|
seen.add(profile.actor.did)
|
||||||
combined.push(profile)
|
combined.push(profile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return combined
|
return combined
|
||||||
}, [data?.suggestions, moreSuggestions?.pages, did])
|
}, [data?.suggestions, moreSuggestions?.pages, did, data?.recId])
|
||||||
|
|
||||||
const filteredProfiles = React.useMemo(() => {
|
const filteredProfiles = useMemo(() => {
|
||||||
return allProfiles.filter(p => !dismissedDids.has(p.did))
|
return allProfiles.filter(p => !dismissedDids.has(p.actor.did))
|
||||||
}, [allProfiles, dismissedDids])
|
}, [allProfiles, dismissedDids])
|
||||||
|
|
||||||
// Fetch more when running low
|
// Fetch more when running low
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
moderationOpts &&
|
moderationOpts &&
|
||||||
filteredProfiles.length < maxLength &&
|
filteredProfiles.length < maxLength &&
|
||||||
hasNextPage &&
|
hasNextPage &&
|
||||||
!isFetchingNextPage
|
!isFetchingNextPage
|
||||||
) {
|
) {
|
||||||
fetchNextPage()
|
void fetchNextPage()
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
filteredProfiles.length,
|
filteredProfiles.length,
|
||||||
@@ -301,11 +295,9 @@ export function SuggestedFollowsProfile({did}: {did: string}) {
|
|||||||
isSuggestionsLoading={isSuggestionsLoading}
|
isSuggestionsLoading={isSuggestionsLoading}
|
||||||
profiles={filteredProfiles}
|
profiles={filteredProfiles}
|
||||||
totalProfileCount={allProfiles.length}
|
totalProfileCount={allProfiles.length}
|
||||||
recId={data?.recId}
|
|
||||||
error={error}
|
error={error}
|
||||||
viewContext="profile"
|
viewContext="profile"
|
||||||
onDismiss={onDismiss}
|
onDismiss={onDismiss}
|
||||||
dismissingDids={dismissingDids}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -327,46 +319,36 @@ export function SuggestedFollowsHome() {
|
|||||||
error: suggestionsError,
|
error: suggestionsError,
|
||||||
} = useSuggestedFollowsQuery({limit: 25})
|
} = useSuggestedFollowsQuery({limit: 25})
|
||||||
|
|
||||||
const [dismissedDids, setDismissedDids] = React.useState<Set<string>>(
|
const [dismissedDids, setDismissedDids] = useState<Set<string>>(new Set())
|
||||||
new Set(),
|
|
||||||
)
|
|
||||||
const [dismissingDids, setDismissingDids] = React.useState<Set<string>>(
|
|
||||||
new Set(),
|
|
||||||
)
|
|
||||||
|
|
||||||
const onDismiss = React.useCallback((did: string) => {
|
const onDismiss = 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))
|
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 experimental query with paginated suggestions
|
// Combine profiles from experimental query with paginated suggestions
|
||||||
const allProfiles = React.useMemo(() => {
|
const allProfiles = useMemo(() => {
|
||||||
const fallbackProfiles =
|
const fallbackProfiles =
|
||||||
moreSuggestions?.pages.flatMap(page => page.actors) ?? []
|
moreSuggestions?.pages.flatMap(page =>
|
||||||
|
page.actors.map(actor => ({actor, recId: page.recId})),
|
||||||
|
) ?? []
|
||||||
|
|
||||||
// Dedupe by did, preferring experimental profiles
|
// Dedupe by did, preferring experimental profiles
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
const combined: bsky.profile.AnyProfileView[] = []
|
const combined: Array<{
|
||||||
|
actor: bsky.profile.AnyProfileView
|
||||||
|
recId?: number
|
||||||
|
}> = []
|
||||||
|
|
||||||
for (const profile of experimentalProfiles) {
|
for (const profile of experimentalProfiles) {
|
||||||
if (!seen.has(profile.did)) {
|
if (!seen.has(profile.did)) {
|
||||||
seen.add(profile.did)
|
seen.add(profile.did)
|
||||||
combined.push(profile)
|
combined.push({actor: profile, recId: undefined})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const profile of fallbackProfiles) {
|
for (const profile of fallbackProfiles) {
|
||||||
if (!seen.has(profile.did)) {
|
if (!seen.has(profile.actor.did)) {
|
||||||
seen.add(profile.did)
|
seen.add(profile.actor.did)
|
||||||
combined.push(profile)
|
combined.push(profile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,19 +356,19 @@ export function SuggestedFollowsHome() {
|
|||||||
return combined
|
return combined
|
||||||
}, [experimentalProfiles, moreSuggestions?.pages])
|
}, [experimentalProfiles, moreSuggestions?.pages])
|
||||||
|
|
||||||
const filteredProfiles = React.useMemo(() => {
|
const filteredProfiles = useMemo(() => {
|
||||||
return allProfiles.filter(p => !dismissedDids.has(p.did))
|
return allProfiles.filter(p => !dismissedDids.has(p.actor.did))
|
||||||
}, [allProfiles, dismissedDids])
|
}, [allProfiles, dismissedDids])
|
||||||
|
|
||||||
// Fetch more when running low
|
// Fetch more when running low
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
moderationOpts &&
|
moderationOpts &&
|
||||||
filteredProfiles.length < maxLength &&
|
filteredProfiles.length < maxLength &&
|
||||||
hasNextPage &&
|
hasNextPage &&
|
||||||
!isFetchingNextPage
|
!isFetchingNextPage
|
||||||
) {
|
) {
|
||||||
fetchNextPage()
|
void fetchNextPage()
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
filteredProfiles.length,
|
filteredProfiles.length,
|
||||||
@@ -405,7 +387,6 @@ export function SuggestedFollowsHome() {
|
|||||||
error={experimentalError || suggestionsError}
|
error={experimentalError || suggestionsError}
|
||||||
viewContext="feed"
|
viewContext="feed"
|
||||||
onDismiss={onDismiss}
|
onDismiss={onDismiss}
|
||||||
dismissingDids={dismissingDids}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -415,16 +396,13 @@ export function ProfileGrid({
|
|||||||
error,
|
error,
|
||||||
profiles,
|
profiles,
|
||||||
totalProfileCount,
|
totalProfileCount,
|
||||||
recId,
|
|
||||||
viewContext = 'feed',
|
viewContext = 'feed',
|
||||||
onDismiss,
|
onDismiss,
|
||||||
dismissingDids,
|
|
||||||
isVisible = true,
|
isVisible = true,
|
||||||
}: {
|
}: {
|
||||||
isSuggestionsLoading: boolean
|
isSuggestionsLoading: boolean
|
||||||
profiles: bsky.profile.AnyProfileView[]
|
profiles: {actor: bsky.profile.AnyProfileView; recId?: number}[]
|
||||||
totalProfileCount?: number
|
totalProfileCount?: number
|
||||||
recId?: number
|
|
||||||
error: Error | null
|
error: Error | null
|
||||||
dismissingDids?: Set<string>
|
dismissingDids?: Set<string>
|
||||||
viewContext: 'profile' | 'profileHeader' | 'feed'
|
viewContext: 'profile' | 'profileHeader' | 'feed'
|
||||||
@@ -463,18 +441,18 @@ export function ProfileGrid({
|
|||||||
|
|
||||||
const profilesToShow = profiles.slice(0, maxLength)
|
const profilesToShow = profiles.slice(0, maxLength)
|
||||||
profilesToShow.forEach((profile, index) => {
|
profilesToShow.forEach((profile, index) => {
|
||||||
if (!seenProfilesRef.current.has(profile.did)) {
|
if (!seenProfilesRef.current.has(profile.actor.did)) {
|
||||||
seenProfilesRef.current.add(profile.did)
|
seenProfilesRef.current.add(profile.actor.did)
|
||||||
ax.metric('suggestedUser:seen', {
|
ax.metric('suggestedUser:seen', {
|
||||||
logContext,
|
logContext,
|
||||||
recId,
|
recId: profile.recId,
|
||||||
position: index,
|
position: index,
|
||||||
suggestedDid: profile.did,
|
suggestedDid: profile.actor.did,
|
||||||
category: null,
|
category: null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [ax, isLoading, error, profiles, maxLength, logContext, recId])
|
}, [ax, isLoading, error, profiles, maxLength, logContext])
|
||||||
|
|
||||||
// For profile header, fire when isVisible becomes true
|
// For profile header, fire when isVisible becomes true
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -540,8 +518,15 @@ export function ProfileGrid({
|
|||||||
? null
|
? null
|
||||||
: profiles.slice(0, maxLength).map((profile, index) => (
|
: profiles.slice(0, maxLength).map((profile, index) => (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
key={profile.did}
|
key={profile.actor.did}
|
||||||
layout={LinearTransition.duration(DISMISS_ANIMATION_DURATION)}
|
layout={native(
|
||||||
|
LinearTransition.delay(DISMISS_ANIMATION_DURATION).easing(
|
||||||
|
Easing.out(Easing.exp),
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
exiting={FadeOut.duration(DISMISS_ANIMATION_DURATION)}
|
||||||
|
// for web, as the cards are static, not in a list
|
||||||
|
entering={web(FadeIn.delay(DISMISS_ANIMATION_DURATION * 2))}
|
||||||
style={[
|
style={[
|
||||||
a.flex_1,
|
a.flex_1,
|
||||||
gtMobile &&
|
gtMobile &&
|
||||||
@@ -550,22 +535,17 @@ export function ProfileGrid({
|
|||||||
a.flex_grow,
|
a.flex_grow,
|
||||||
{width: `calc(30% - ${a.gap_md.gap / 2}px)`},
|
{width: `calc(30% - ${a.gap_md.gap / 2}px)`},
|
||||||
]),
|
]),
|
||||||
{
|
|
||||||
opacity: dismissingDids?.has(profile.did) ? 0 : 1,
|
|
||||||
transitionProperty: 'opacity',
|
|
||||||
transitionDuration: `${DISMISS_ANIMATION_DURATION}ms`,
|
|
||||||
},
|
|
||||||
]}>
|
]}>
|
||||||
<ProfileCard.Link
|
<ProfileCard.Link
|
||||||
profile={profile}
|
profile={profile.actor}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
ax.metric('suggestedUser:press', {
|
ax.metric('suggestedUser:press', {
|
||||||
logContext: isFeedContext
|
logContext: isFeedContext
|
||||||
? 'InterstitialDiscover'
|
? 'InterstitialDiscover'
|
||||||
: 'InterstitialProfile',
|
: 'InterstitialProfile',
|
||||||
recId,
|
recId: profile.recId,
|
||||||
position: index,
|
position: index,
|
||||||
suggestedDid: profile.did,
|
suggestedDid: profile.actor.did,
|
||||||
category: null,
|
category: null,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
@@ -581,14 +561,14 @@ export function ProfileGrid({
|
|||||||
label={_(msg`Dismiss this suggestion`)}
|
label={_(msg`Dismiss this suggestion`)}
|
||||||
onPress={e => {
|
onPress={e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
onDismiss(profile.did)
|
onDismiss(profile.actor.did)
|
||||||
ax.metric('suggestedUser:dismiss', {
|
ax.metric('suggestedUser:dismiss', {
|
||||||
logContext: isFeedContext
|
logContext: isFeedContext
|
||||||
? 'InterstitialDiscover'
|
? 'InterstitialDiscover'
|
||||||
: 'InterstitialProfile',
|
: 'InterstitialProfile',
|
||||||
position: index,
|
position: index,
|
||||||
suggestedDid: profile.did,
|
suggestedDid: profile.actor.did,
|
||||||
recId,
|
recId: profile.recId,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
style={[
|
style={[
|
||||||
@@ -621,18 +601,18 @@ export function ProfileGrid({
|
|||||||
a.mb_auto,
|
a.mb_auto,
|
||||||
]}>
|
]}>
|
||||||
<ProfileCard.Avatar
|
<ProfileCard.Avatar
|
||||||
profile={profile}
|
profile={profile.actor}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
disabledPreview
|
disabledPreview
|
||||||
size={88}
|
size={88}
|
||||||
/>
|
/>
|
||||||
<View style={[a.flex_col, a.align_center, a.max_w_full]}>
|
<View style={[a.flex_col, a.align_center, a.max_w_full]}>
|
||||||
<ProfileCard.Name
|
<ProfileCard.Name
|
||||||
profile={profile}
|
profile={profile.actor}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
/>
|
/>
|
||||||
<ProfileCard.Description
|
<ProfileCard.Description
|
||||||
profile={profile}
|
profile={profile.actor}
|
||||||
numberOfLines={2}
|
numberOfLines={2}
|
||||||
style={[
|
style={[
|
||||||
t.atoms.text_contrast_medium,
|
t.atoms.text_contrast_medium,
|
||||||
@@ -644,7 +624,7 @@ export function ProfileGrid({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ProfileCard.FollowButton
|
<ProfileCard.FollowButton
|
||||||
profile={profile}
|
profile={profile.actor}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
logContext="FeedInterstitial"
|
logContext="FeedInterstitial"
|
||||||
withIcon={false}
|
withIcon={false}
|
||||||
@@ -655,9 +635,9 @@ export function ProfileGrid({
|
|||||||
? 'InterstitialDiscover'
|
? 'InterstitialDiscover'
|
||||||
: 'InterstitialProfile',
|
: 'InterstitialProfile',
|
||||||
location: 'Card',
|
location: 'Card',
|
||||||
recId,
|
recId: profile.recId,
|
||||||
position: index,
|
position: index,
|
||||||
suggestedDid: profile.did,
|
suggestedDid: profile.actor.did,
|
||||||
category: null,
|
category: null,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
@@ -726,6 +706,7 @@ export function ProfileGrid({
|
|||||||
|
|
||||||
<FollowDialogWithoutGuide control={followDialogControl} />
|
<FollowDialogWithoutGuide control={followDialogControl} />
|
||||||
|
|
||||||
|
<LayoutAnimationConfig skipExiting skipEntering>
|
||||||
{gtMobile ? (
|
{gtMobile ? (
|
||||||
<View style={[a.p_lg, a.pt_md]}>
|
<View style={[a.p_lg, a.pt_md]}>
|
||||||
<View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}>
|
<View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}>
|
||||||
@@ -755,6 +736,7 @@ export function ProfileGrid({
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
</BlockDrawerGesture>
|
</BlockDrawerGesture>
|
||||||
)}
|
)}
|
||||||
|
</LayoutAnimationConfig>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -795,7 +777,7 @@ export function SuggestedFeeds() {
|
|||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
|
|
||||||
const feeds = React.useMemo(() => {
|
const feeds = useMemo(() => {
|
||||||
const items: AppBskyFeedDefs.GeneratorView[] = []
|
const items: AppBskyFeedDefs.GeneratorView[] = []
|
||||||
|
|
||||||
if (!data) return items
|
if (!data) return items
|
||||||
|
|||||||
Reference in New Issue
Block a user