Compare commits

...

1 Commits

Author SHA1 Message Date
Samuel Newman 92b77b2584 refreshed suggested follows in profile 2025-07-11 16:38:29 +03:00
5 changed files with 222 additions and 14 deletions
+1
View File
@@ -545,6 +545,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
a.flex_row,
a.align_center,
a.justify_center,
a.curve_continuous,
flattenedBaseStyles,
...(state.hovered || state.pressed
? [hoverStyles, flatten(hoverStyleProp)]
+18 -9
View File
@@ -1,5 +1,10 @@
import React from 'react'
import {type GestureResponderEvent, View} from 'react-native'
import {useMemo} from 'react'
import {
type GestureResponderEvent,
type StyleProp,
type TextStyle,
View,
} from 'react-native'
import {
moderateProfile,
type ModerationOpts,
@@ -136,12 +141,14 @@ export function Avatar({
onPress,
disabledPreview,
liveOverride,
size = 40,
}: {
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
onPress?: () => void
disabledPreview?: boolean
liveOverride?: boolean
size?: number
}) {
const moderation = moderateProfile(profile, moderationOpts)
@@ -149,7 +156,7 @@ export function Avatar({
return disabledPreview ? (
<UserAvatar
size={40}
size={size}
avatar={profile.avatar}
type={profile.associated?.labeler ? 'labeler' : 'user'}
moderation={moderation.ui('avatar')}
@@ -157,7 +164,7 @@ export function Avatar({
/>
) : (
<PreviewableUserAvatar
size={40}
size={size}
profile={profile}
moderation={moderation.ui('avatar')}
onBeforePress={onPress}
@@ -166,7 +173,7 @@ export function Avatar({
)
}
export function AvatarPlaceholder() {
export function AvatarPlaceholder({size = 40}: {size?: number}) {
const t = useTheme()
return (
<View
@@ -174,8 +181,8 @@ export function AvatarPlaceholder() {
a.rounded_full,
t.atoms.bg_contrast_25,
{
width: 40,
height: 40,
width: size,
height: size,
},
]}
/>
@@ -340,12 +347,14 @@ export function NameAndHandlePlaceholder() {
export function Description({
profile: profileUnshadowed,
numberOfLines = 3,
style,
}: {
profile: bsky.profile.AnyProfileView
numberOfLines?: number
style?: StyleProp<TextStyle>
}) {
const profile = useProfileShadow(profileUnshadowed)
const rt = React.useMemo(() => {
const rt = useMemo(() => {
if (!('description' in profile)) return
const rt = new RichTextApi({text: profile.description || ''})
rt.detectFacetsWithoutResolution()
@@ -363,7 +372,7 @@ export function Description({
<View style={[a.pt_xs]}>
<RichText
value={rt}
style={[a.leading_snug]}
style={[a.leading_snug, style]}
numberOfLines={numberOfLines}
disableLinks
/>
+6 -3
View File
@@ -1,4 +1,3 @@
import {type ReactNode} from 'react'
import {View} from 'react-native'
import {
@@ -15,7 +14,7 @@ type SkeletonProps = {
blend?: boolean
}
export function Text({blend, style}: TextStyleProp & SkeletonProps) {
export function Text({blend, style}: Required<TextStyleProp> & SkeletonProps) {
const {fonts, flags, theme: t} = useAlf()
const {width, ...flattened} = flatten(style)
const {lineHeight = 14, ...rest} = normalizeTextStyles(
@@ -28,7 +27,11 @@ export function Text({blend, style}: TextStyleProp & SkeletonProps) {
)
return (
<View
style={[a.flex_1, {maxWidth: width, paddingVertical: lineHeight * 0.15}]}>
style={[
a.flex_1,
a.w_full,
{maxWidth: width, paddingVertical: lineHeight * 0.15},
]}>
<View
style={[
a.rounded_md,
@@ -1,4 +1,4 @@
import React, {memo, useMemo} from 'react'
import {memo, useCallback, useMemo, useState} from 'react'
import {View} from 'react-native'
import {
type AppBskyActorDefs,
@@ -40,6 +40,7 @@ import {EditProfileDialog} from './EditProfileDialog'
import {ProfileHeaderHandle} from './Handle'
import {ProfileHeaderMetrics} from './Metrics'
import {ProfileHeaderShell} from './Shell'
import {ProfileHeaderSuggestedFollows} from './SuggestedFollows'
interface Props {
profile: AppBskyActorDefs.ProfileViewDetailed
@@ -73,6 +74,7 @@ let ProfileHeaderStandard = ({
const [_queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
const unblockPromptControl = Prompt.usePromptControl()
const requireAuth = useRequireAuth()
const [showSuggestedFollows, _setShowSuggestedFollows] = useState(true)
const isBlockedUser =
profile.viewer?.blocking ||
profile.viewer?.blockedBy ||
@@ -122,7 +124,7 @@ let ProfileHeaderStandard = ({
})
}
const unblockAccount = React.useCallback(async () => {
const unblockAccount = useCallback(async () => {
try {
await queueUnblock()
Toast.show(_(msg({message: 'Account unblocked', context: 'toast'})))
@@ -308,6 +310,9 @@ let ProfileHeaderStandard = ({
</View>
)}
</View>
{showSuggestedFollows && (
<ProfileHeaderSuggestedFollows actorDid={profile.did} />
)}
<Prompt.Basic
control={unblockPromptControl}
title={_(msg`Unblock Account?`)}
@@ -0,0 +1,190 @@
import {ScrollView, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
import {atoms as a, tokens, useTheme, type ViewStyleProp} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as ProfileCard from '#/components/ProfileCard'
import * as Skellie from '#/components/Skeleton'
import {Text} from '#/components/Typography'
const INNER_PADDING = tokens.space.lg
const CARD_HEIGHT = 200
const MOBILE_CARD_WIDTH = 150
function CardOuter({
children,
style,
}: {children: React.ReactNode | React.ReactNode[]} & ViewStyleProp) {
const t = useTheme()
return (
<View
style={[
a.p_md,
a.align_center,
a.justify_between,
a.rounded_lg,
a.curve_continuous,
a.border,
t.atoms.bg,
t.atoms.border_contrast_low,
{height: CARD_HEIGHT, width: MOBILE_CARD_WIDTH},
style,
]}>
{children}
</View>
)
}
export function SuggestedFollowPlaceholder() {
return (
<CardOuter>
<Skellie.Circle size={64} />
<View
style={[
a.w_full,
a.flex_col,
a.align_center,
a.flex_1,
a.pt_md,
a.pb_sm,
]}>
<Skellie.Text style={[a.text_sm, {width: '60%'}]} />
<Skellie.Text style={[a.text_sm, {width: '90%'}]} />
<Skellie.Text style={[a.text_sm, {width: '80%'}]} />
</View>
<Skellie.Pill size={33} style={[a.rounded_sm, a.w_full, a.flex_grow_0]} />
</CardOuter>
)
}
export function ProfileHeaderSuggestedFollows({actorDid}: {actorDid: string}) {
const t = useTheme()
const {_} = useLingui()
const {isPending: isSuggestionsPending, data} =
useSuggestedFollowsByActorQuery({did: actorDid})
const moderationOpts = useModerationOpts()
const isPending = isSuggestionsPending || !moderationOpts
const followAll = () => {}
return (
<>
<View
style={[
a.flex_row,
a.justify_between,
a.align_center,
a.pb_xs,
{
paddingLeft: INNER_PADDING,
paddingRight: INNER_PADDING / 2,
},
]}>
<Text style={[a.text_sm, a.font_bold]}>
<Trans>Suggested for you</Trans>
</Text>
<Button
onPress={followAll}
hitSlop={10}
label={_(msg`Follow all`)}
size="small"
variant="ghost"
color="primary">
<ButtonText>
<Trans>Follow all</Trans>
</ButtonText>
</Button>
</View>
<View style={[{height: CARD_HEIGHT}]}>
<BlockDrawerGesture>
<ScrollView
horizontal={true}
showsHorizontalScrollIndicator={isWeb}
persistentScrollbar={true}
scrollIndicatorInsets={{bottom: 0}}
decelerationRate="fast"
contentContainerStyle={[
a.flex_row,
a.gap_sm,
{
paddingHorizontal: INNER_PADDING,
},
]}>
{isPending ? (
<>
<SuggestedFollowPlaceholder />
<SuggestedFollowPlaceholder />
<SuggestedFollowPlaceholder />
<SuggestedFollowPlaceholder />
<SuggestedFollowPlaceholder />
</>
) : data ? (
data.suggestions
.filter(s => (s.associated?.labeler ? false : true))
.map(profile => (
<ProfileCard.Link
key={profile.did}
profile={profile}
onPress={() => {
logger.metric(
'profile:header:suggestedFollowsCard:press',
{},
)
}}
style={[a.flex_1]}>
{({hovered, pressed}) => (
<CardOuter
style={[
a.flex_1,
(hovered || pressed) && t.atoms.border_contrast_high,
]}>
<ProfileCard.Avatar
size={64}
profile={profile}
moderationOpts={moderationOpts}
/>
<View style={[a.flex_grow_0, a.pt_sm]}>
<ProfileCard.Name
profile={profile}
moderationOpts={moderationOpts}
/>
</View>
<View
style={[
a.flex_grow_0,
{minHeight: 38},
a.justify_start,
]}>
<ProfileCard.Description
profile={profile}
numberOfLines={2}
style={[a.text_center]}
/>
</View>
<View style={[a.pt_md, a.flex_grow, a.w_full]}>
<ProfileCard.FollowButton
profile={profile}
moderationOpts={moderationOpts}
logContext="ProfileHeaderSuggestedFollows"
color="primary"
withIcon={false}
/>
</View>
</CardOuter>
)}
</ProfileCard.Link>
))
) : null}
</ScrollView>
</BlockDrawerGesture>
</View>
</>
)
}