-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor onboarding suggested follows #1897
Changes from 2 commits
282a1fa
2d34d9f
2221f72
9f3d8e0
2549f9e
e84772d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import {AppBskyActorGetSuggestions, moderateProfile} from '@atproto/api' | ||
import { | ||
useInfiniteQuery, | ||
useMutation, | ||
InfiniteData, | ||
QueryKey, | ||
} from '@tanstack/react-query' | ||
|
||
import {useSession} from '#/state/session' | ||
import {useModerationOpts} from '#/state/queries/preferences' | ||
|
||
export const suggestedFollowsQueryKey = ['suggested-follows'] | ||
|
||
export function useSuggestedFollowsQuery() { | ||
const {agent, currentAccount} = useSession() | ||
const moderationOpts = useModerationOpts() | ||
|
||
return useInfiniteQuery< | ||
AppBskyActorGetSuggestions.OutputSchema, | ||
Error, | ||
InfiniteData<AppBskyActorGetSuggestions.OutputSchema>, | ||
QueryKey, | ||
string | undefined | ||
>({ | ||
enabled: !!moderationOpts, | ||
queryKey: suggestedFollowsQueryKey, | ||
queryFn: async ({pageParam}) => { | ||
const res = await agent.app.bsky.actor.getSuggestions({ | ||
limit: 25, | ||
cursor: pageParam, | ||
}) | ||
|
||
res.data.actors = res.data.actors | ||
.filter( | ||
actor => !moderateProfile(actor, moderationOpts!).account.filter, | ||
) | ||
.filter(actor => { | ||
const viewer = actor.viewer | ||
if (viewer) { | ||
if ( | ||
viewer.following || | ||
viewer.muted || | ||
viewer.mutedByList || | ||
viewer.blockedBy || | ||
viewer.blocking | ||
) { | ||
return false | ||
} | ||
} | ||
if (actor.did === currentAccount?.did) { | ||
return false | ||
} | ||
return true | ||
}) | ||
|
||
// TODO refactor — hydrate follow cache here? | ||
|
||
return res.data | ||
}, | ||
initialPageParam: undefined, | ||
getNextPageParam: lastPage => lastPage.cursor, | ||
}) | ||
} | ||
|
||
export function useGetSuggestedFollowersByActor() { | ||
const {agent} = useSession() | ||
|
||
return useMutation({ | ||
mutationFn: async (actor: string) => { | ||
const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({ | ||
actor: actor, | ||
}) | ||
|
||
return res.data | ||
}, | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,31 +2,35 @@ 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 {AppBskyActorDefs, moderateProfile} from '@atproto/api' | ||
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' | ||
import {SuggestedActorsModel} from '#/state/models/discovery/suggested-actors' | ||
import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows' | ||
import {useGetSuggestedFollowersByActor} from '#/state/queries/suggested-follows' | ||
import {useModerationOpts} from '#/state/queries/preferences' | ||
|
||
type Props = { | ||
next: () => void | ||
} | ||
export const RecommendedFollows = observer(function RecommendedFollowsImpl({ | ||
next, | ||
}: Props) { | ||
const store = useStores() | ||
const pal = usePalette('default') | ||
const {isTabletOrMobile} = useWebMediaQueries() | ||
const suggestedActors = React.useMemo(() => { | ||
const model = new SuggestedActorsModel(store) | ||
model.refresh() | ||
return model | ||
}, [store]) | ||
const {data: suggestedFollows, dataUpdatedAt} = useSuggestedFollowsQuery() | ||
const {mutateAsync: getSuggestedFollowsByActor} = | ||
useGetSuggestedFollowersByActor() | ||
const [additionalSuggestions, setAdditionalSuggestions] = React.useState<{ | ||
[did: string]: AppBskyActorDefs.ProfileView[] | ||
}>({}) | ||
const existingDids = React.useRef<string[]>([]) | ||
const moderationOpts = useModerationOpts() | ||
|
||
const title = ( | ||
<> | ||
|
@@ -84,6 +88,59 @@ export const RecommendedFollows = observer(function RecommendedFollowsImpl({ | |
</> | ||
) | ||
|
||
const suggestions = React.useMemo(() => { | ||
if (!suggestedFollows) return [] | ||
|
||
const additional = Object.entries(additionalSuggestions) | ||
const items = [] | ||
|
||
for (const page of suggestedFollows.pages) { | ||
for (const actor of page.actors) { | ||
items.push(actor) | ||
} | ||
} | ||
|
||
outer: while (additional.length) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so rare that you get to use labels in ts, love it when you do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so satisfying |
||
const additionalAccount = additional.shift() | ||
|
||
if (!additionalAccount) break | ||
|
||
const [followedUser, relatedAccounts] = additionalAccount | ||
|
||
for (let i = 0; i < items.length; i++) { | ||
if (items[i].did === followedUser) { | ||
items.splice(i + 1, 0, ...relatedAccounts) | ||
continue outer | ||
} | ||
} | ||
} | ||
|
||
existingDids.current = items.map(i => i.did) | ||
|
||
return items | ||
}, [suggestedFollows, additionalSuggestions]) | ||
|
||
const onFollowStateChange = React.useCallback( | ||
async ({following, did}: {following: boolean; did: string}) => { | ||
if (following) { | ||
const {suggestions: results} = await getSuggestedFollowsByActor(did) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should wrap this in a try/catch if just to log or suppress it, so we dont have uncaughts |
||
|
||
if (results.length) { | ||
const deduped = results.filter( | ||
r => !existingDids.current.find(did => did === r.did), | ||
) | ||
setAdditionalSuggestions(s => ({ | ||
...s, | ||
[did]: deduped.slice(0, 3), | ||
})) | ||
} | ||
} | ||
|
||
// not handling the unfollow case | ||
}, | ||
[existingDids, getSuggestedFollowsByActor, setAdditionalSuggestions], | ||
) | ||
|
||
return ( | ||
<> | ||
<TabletOrDesktop> | ||
|
@@ -93,21 +150,20 @@ export const RecommendedFollows = observer(function RecommendedFollowsImpl({ | |
horizontal | ||
titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}} | ||
contentStyle={{paddingHorizontal: 0}}> | ||
{suggestedActors.isLoading ? ( | ||
{!suggestedFollows || !moderationOpts ? ( | ||
<ActivityIndicator size="large" /> | ||
) : ( | ||
<FlatList | ||
data={suggestedActors.suggestions} | ||
renderItem={({item, index}) => ( | ||
data={suggestions} | ||
renderItem={({item}) => ( | ||
<RecommendedFollowsItem | ||
item={item} | ||
index={index} | ||
insertSuggestionsByActor={suggestedActors.insertSuggestionsByActor.bind( | ||
suggestedActors, | ||
)} | ||
profile={item} | ||
dataUpdatedAt={dataUpdatedAt} | ||
onFollowStateChange={onFollowStateChange} | ||
moderation={moderateProfile(item, moderationOpts)} | ||
/> | ||
)} | ||
keyExtractor={(item, index) => item.did + index.toString()} | ||
keyExtractor={item => item.did} | ||
style={{flex: 1}} | ||
/> | ||
)} | ||
|
@@ -127,21 +183,20 @@ export const RecommendedFollows = observer(function RecommendedFollowsImpl({ | |
users. | ||
</Text> | ||
</View> | ||
{suggestedActors.isLoading ? ( | ||
{!suggestedFollows || !moderationOpts ? ( | ||
<ActivityIndicator size="large" /> | ||
) : ( | ||
<FlatList | ||
data={suggestedActors.suggestions} | ||
renderItem={({item, index}) => ( | ||
data={suggestions} | ||
renderItem={({item}) => ( | ||
<RecommendedFollowsItem | ||
item={item} | ||
index={index} | ||
insertSuggestionsByActor={suggestedActors.insertSuggestionsByActor.bind( | ||
suggestedActors, | ||
)} | ||
profile={item} | ||
dataUpdatedAt={dataUpdatedAt} | ||
onFollowStateChange={onFollowStateChange} | ||
moderation={moderateProfile(item, moderationOpts)} | ||
/> | ||
)} | ||
keyExtractor={(item, index) => item.did + index.toString()} | ||
keyExtractor={item => item.did} | ||
style={{flex: 1}} | ||
/> | ||
)} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pfrazee just double checking that we won't need to do this anymore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can go