Bluesky app fork with some witchin' additions 馃挮
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}