An ATproto social media client -- with an independent Appview.
at main 2.6 kB view raw
1import {View} from 'react-native' 2import {moderateProfile} from '@atproto/api' 3 4import {logger} from '#/logger' 5import {useModerationOpts} from '#/state/preferences/moderation-opts' 6import {useProfilesQuery} from '#/state/queries/profile' 7import {UserAvatar} from '#/view/com/util/UserAvatar' 8import {atoms as a, useTheme} from '#/alf' 9import type * as bsky from '#/types/bsky' 10 11export function AvatarStack({ 12 profiles, 13 size = 26, 14 numPending, 15 backgroundColor, 16}: { 17 profiles: bsky.profile.AnyProfileView[] 18 size?: number 19 numPending?: number 20 backgroundColor?: string 21}) { 22 const translation = size / 3 // overlap by 1/3 23 const t = useTheme() 24 const moderationOpts = useModerationOpts() 25 26 const isPending = (numPending && profiles.length === 0) || !moderationOpts 27 28 const items = isPending 29 ? Array.from({length: numPending ?? profiles.length}).map((_, i) => ({ 30 key: i, 31 profile: null, 32 moderation: null, 33 })) 34 : profiles.map(item => ({ 35 key: item.did, 36 profile: item, 37 moderation: moderateProfile(item, moderationOpts), 38 })) 39 40 return ( 41 <View 42 style={[ 43 a.flex_row, 44 a.align_center, 45 a.relative, 46 {width: size + (items.length - 1) * (size - translation)}, 47 ]}> 48 {items.map((item, i) => ( 49 <View 50 key={item.key} 51 style={[ 52 t.atoms.bg_contrast_25, 53 a.relative, 54 { 55 width: size, 56 height: size, 57 left: i * -translation, 58 borderWidth: 1, 59 borderColor: backgroundColor ?? t.atoms.bg.backgroundColor, 60 borderRadius: 999, 61 zIndex: 3 - i, 62 }, 63 ]}> 64 {item.profile && ( 65 <UserAvatar 66 size={size - 2} 67 avatar={item.profile.avatar} 68 type={item.profile.associated?.labeler ? 'labeler' : 'user'} 69 moderation={item.moderation.ui('avatar')} 70 /> 71 )} 72 </View> 73 ))} 74 </View> 75 ) 76} 77 78export function AvatarStackWithFetch({ 79 profiles, 80 size, 81 backgroundColor, 82}: { 83 profiles: string[] 84 size?: number 85 backgroundColor?: string 86}) { 87 const {data, error} = useProfilesQuery({handles: profiles}) 88 89 if (error) { 90 if (error.name !== 'AbortError') { 91 logger.error('Error fetching profiles for AvatarStack', { 92 safeMessage: error, 93 }) 94 } 95 return null 96 } 97 98 return ( 99 <AvatarStack 100 numPending={profiles.length} 101 profiles={data?.profiles || []} 102 size={size} 103 backgroundColor={backgroundColor} 104 /> 105 ) 106}