WIP suggested follows section for feeds
This commit is contained in:
@@ -0,0 +1,156 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {ScrollView} from 'react-native-gesture-handler'
|
||||||
|
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
|
import {useModerationOpts} from '#/state/queries/preferences'
|
||||||
|
import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
|
||||||
|
import {isJustAMute} from 'lib/moderation'
|
||||||
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
|
import {sanitizeHandle} from 'lib/strings/handles'
|
||||||
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
|
import {useFollowMethods} from '#/components/hooks/useFollowMethods'
|
||||||
|
import {useRichText} from '#/components/hooks/useRichText'
|
||||||
|
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
|
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
|
import {RichText} from '#/components/RichText'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
export function SuggestedFollowCard({
|
||||||
|
profile: profileUnshadowed,
|
||||||
|
}: {
|
||||||
|
profile: AppBskyActorDefs.ProfileViewBasic
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const profile = useProfileShadow(profileUnshadowed)
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
|
const {follow, unfollow} = useFollowMethods({
|
||||||
|
profile,
|
||||||
|
// @ts-ignore TODO
|
||||||
|
logContext: 'FeedSuggestedFollowsCard',
|
||||||
|
})
|
||||||
|
// @ts-ignore TODO
|
||||||
|
const [descriptionRT] = useRichText(profileUnshadowed?.description ?? '')
|
||||||
|
|
||||||
|
if (!moderationOpts) return null
|
||||||
|
const moderation = moderateProfile(profile, moderationOpts)
|
||||||
|
const modui = moderation.ui('profileList')
|
||||||
|
if (modui.filter && !isJustAMute(modui)) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.p_lg,
|
||||||
|
a.rounded_md,
|
||||||
|
a.gap_sm,
|
||||||
|
t.atoms.bg,
|
||||||
|
{
|
||||||
|
width: 300,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||||
|
<PreviewableUserAvatar
|
||||||
|
size={40}
|
||||||
|
did={profile.did}
|
||||||
|
handle={profile.handle}
|
||||||
|
avatar={profile.avatar}
|
||||||
|
moderation={moderation.ui('avatar')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_between,
|
||||||
|
a.gap_lg,
|
||||||
|
a.flex_1,
|
||||||
|
]}>
|
||||||
|
<View style={[a.gap_2xs, a.flex_1]}>
|
||||||
|
<Text
|
||||||
|
style={[a.text_md, a.font_bold, a.leading_tight, a.flex_1]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
{sanitizeDisplayName(
|
||||||
|
profile.displayName || sanitizeHandle(profile.handle),
|
||||||
|
moderation.ui('displayName'),
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[t.atoms.text_contrast_medium, a.flex_1]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
{sanitizeHandle(profile.handle, '@')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={
|
||||||
|
profile.viewer?.following ? _(msg`Following`) : _(msg`Follow`)
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
shape="round"
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
onPress={profile.viewer?.following ? unfollow : follow}>
|
||||||
|
{profile.viewer?.following ? (
|
||||||
|
<ButtonIcon icon={Check} />
|
||||||
|
) : (
|
||||||
|
<ButtonIcon icon={Plus} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={[a.flex_1]} numberOfLines={2}>
|
||||||
|
<RichText value={descriptionRT} style={[t.atoms.text_contrast_high]} />
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeedSuggestedFollows() {
|
||||||
|
const t = useTheme()
|
||||||
|
const {isLoading, data: suggestions, error} = useSuggestedFollowsQuery()
|
||||||
|
|
||||||
|
const profiles: AppBskyActorDefs.ProfileViewBasic[] = []
|
||||||
|
if (suggestions) {
|
||||||
|
// Currently the responses contain duplicate items.
|
||||||
|
// Needs to be fixed on backend, but let's dedupe to be safe.
|
||||||
|
let seen = new Set()
|
||||||
|
for (const page of suggestions.pages) {
|
||||||
|
for (const actor of page.actors) {
|
||||||
|
if (!seen.has(actor.did)) {
|
||||||
|
seen.add(actor.did)
|
||||||
|
profiles.push(actor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25]}>
|
||||||
|
<View style={[a.pt_xl, a.px_lg, a.flex_row, a.gap_md]}>
|
||||||
|
<Text style={[a.font_bold, t.atoms.text_contrast_medium]}>
|
||||||
|
Suggested for you
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||||
|
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
|
||||||
|
{isLoading
|
||||||
|
? null
|
||||||
|
: error || !profiles.length
|
||||||
|
? null
|
||||||
|
: profiles.map((profile, i) => (
|
||||||
|
<SuggestedFollowCard key={i} profile={profile} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import {useSession} from '#/state/session'
|
|||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
|
import {FeedSuggestedFollows} from '#/components/FeedSuggestedFollows'
|
||||||
import {List, ListRef} from '../util/List'
|
import {List, ListRef} from '../util/List'
|
||||||
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||||
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||||
@@ -40,6 +41,7 @@ const LOADING_ITEM = {_reactKey: '__loading__'}
|
|||||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||||
const ERROR_ITEM = {_reactKey: '__error__'}
|
const ERROR_ITEM = {_reactKey: '__error__'}
|
||||||
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
||||||
|
const SUGGESTED_FOLLOWS_ITEM = {_reactKey: '__suggested_follows__'}
|
||||||
|
|
||||||
// DISABLED need to check if this is causing random feed refreshes -prf
|
// DISABLED need to check if this is causing random feed refreshes -prf
|
||||||
// const REFRESH_AFTER = STALE.HOURS.ONE
|
// const REFRESH_AFTER = STALE.HOURS.ONE
|
||||||
@@ -198,6 +200,7 @@ let Feed = ({
|
|||||||
} else if (isEmpty) {
|
} else if (isEmpty) {
|
||||||
arr = arr.concat([EMPTY_FEED_ITEM])
|
arr = arr.concat([EMPTY_FEED_ITEM])
|
||||||
} else if (data) {
|
} else if (data) {
|
||||||
|
arr = arr.concat(SUGGESTED_FOLLOWS_ITEM)
|
||||||
for (const page of data?.pages) {
|
for (const page of data?.pages) {
|
||||||
arr = arr.concat(page.slices)
|
arr = arr.concat(page.slices)
|
||||||
}
|
}
|
||||||
@@ -297,6 +300,8 @@ let Feed = ({
|
|||||||
// see home.ts (feed api) for more info
|
// see home.ts (feed api) for more info
|
||||||
// -prf
|
// -prf
|
||||||
return <DiscoverFallbackHeader />
|
return <DiscoverFallbackHeader />
|
||||||
|
} else if (item === SUGGESTED_FOLLOWS_ITEM) {
|
||||||
|
return <FeedSuggestedFollows />
|
||||||
}
|
}
|
||||||
return <FeedSlice slice={item} />
|
return <FeedSlice slice={item} />
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user