forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}