Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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