mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {View} from 'react-native'
3import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
4import {msg, Plural, Trans} from '@lingui/macro'
5import {useLingui} from '@lingui/react'
6
7import {makeProfileLink} from '#/lib/routes/links'
8import {sanitizeDisplayName} from 'lib/strings/display-names'
9import {UserAvatar} from '#/view/com/util/UserAvatar'
10import {atoms as a, useTheme} from '#/alf'
11import {Link, LinkProps} from '#/components/Link'
12import {Text} from '#/components/Typography'
13
14const AVI_SIZE = 30
15const AVI_SIZE_SMALL = 20
16const AVI_BORDER = 1
17
18/**
19 * Shared logic to determine if `KnownFollowers` should be shown.
20 *
21 * Checks the # of actual returned users instead of the `count` value, because
22 * `count` includes blocked users and `followers` does not.
23 */
24export function shouldShowKnownFollowers(
25 knownFollowers?: AppBskyActorDefs.KnownFollowers,
26) {
27 return knownFollowers && knownFollowers.followers.length > 0
28}
29
30export function KnownFollowers({
31 profile,
32 moderationOpts,
33 onLinkPress,
34 minimal,
35}: {
36 profile: AppBskyActorDefs.ProfileViewDetailed
37 moderationOpts: ModerationOpts
38 onLinkPress?: LinkProps['onPress']
39 minimal?: boolean
40}) {
41 const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
42 new Map(),
43 )
44
45 /*
46 * Results for `knownFollowers` are not sorted consistently, so when
47 * revalidating we can see a flash of this data updating. This cache prevents
48 * this happening for screens that remain in memory. When pushing a new
49 * screen, or once this one is popped, this cache is empty, so new data is
50 * displayed.
51 */
52 if (profile.viewer?.knownFollowers && !cache.current.has(profile.did)) {
53 cache.current.set(profile.did, profile.viewer.knownFollowers)
54 }
55
56 const cachedKnownFollowers = cache.current.get(profile.did)
57
58 if (cachedKnownFollowers && shouldShowKnownFollowers(cachedKnownFollowers)) {
59 return (
60 <KnownFollowersInner
61 profile={profile}
62 cachedKnownFollowers={cachedKnownFollowers}
63 moderationOpts={moderationOpts}
64 onLinkPress={onLinkPress}
65 minimal={minimal}
66 />
67 )
68 }
69
70 return null
71}
72
73function KnownFollowersInner({
74 profile,
75 moderationOpts,
76 cachedKnownFollowers,
77 onLinkPress,
78 minimal,
79}: {
80 profile: AppBskyActorDefs.ProfileViewDetailed
81 moderationOpts: ModerationOpts
82 cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
83 onLinkPress?: LinkProps['onPress']
84 minimal?: boolean
85}) {
86 const t = useTheme()
87 const {_} = useLingui()
88
89 const textStyle = [
90 a.flex_1,
91 a.text_sm,
92 a.leading_snug,
93 t.atoms.text_contrast_medium,
94 ]
95
96 const slice = cachedKnownFollowers.followers.slice(0, 3).map(f => {
97 const moderation = moderateProfile(f, moderationOpts)
98 return {
99 profile: {
100 ...f,
101 displayName: sanitizeDisplayName(
102 f.displayName || f.handle,
103 moderation.ui('displayName'),
104 ),
105 },
106 moderation,
107 }
108 })
109
110 // Does not have blocks applied. Always >= slices.length
111 const serverCount = cachedKnownFollowers.count
112
113 /*
114 * We check above too, but here for clarity and a reminder to _check for
115 * valid indices_
116 */
117 if (slice.length === 0) return null
118
119 const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE
120
121 return (
122 <Link
123 label={_(
124 msg`Press to view followers of this account that you also follow`,
125 )}
126 onPress={onLinkPress}
127 to={makeProfileLink(profile, 'known-followers')}
128 style={[
129 a.flex_1,
130 a.flex_row,
131 minimal ? a.gap_sm : a.gap_md,
132 a.align_center,
133 {marginLeft: -AVI_BORDER},
134 ]}>
135 {({hovered, pressed}) => (
136 <>
137 <View
138 style={[
139 {
140 height: SIZE,
141 width: SIZE + (slice.length - 1) * a.gap_md.gap,
142 },
143 pressed && {
144 opacity: 0.5,
145 },
146 ]}>
147 {slice.map(({profile: prof, moderation}, i) => (
148 <View
149 key={prof.did}
150 style={[
151 a.absolute,
152 a.rounded_full,
153 {
154 borderWidth: AVI_BORDER,
155 borderColor: t.atoms.bg.backgroundColor,
156 width: SIZE + AVI_BORDER * 2,
157 height: SIZE + AVI_BORDER * 2,
158 left: i * a.gap_md.gap,
159 zIndex: AVI_BORDER - i,
160 },
161 ]}>
162 <UserAvatar
163 size={SIZE}
164 avatar={prof.avatar}
165 moderation={moderation.ui('avatar')}
166 />
167 </View>
168 ))}
169 </View>
170
171 <Text
172 style={[
173 textStyle,
174 hovered && {
175 textDecorationLine: 'underline',
176 textDecorationColor: t.atoms.text_contrast_medium.color,
177 },
178 pressed && {
179 opacity: 0.5,
180 },
181 ]}
182 numberOfLines={2}>
183 {slice.length >= 2 ? (
184 // 2-n followers, including blocks
185 serverCount > 2 ? (
186 <Trans>
187 Followed by{' '}
188 <Text key={slice[0].profile.did} style={textStyle}>
189 {slice[0].profile.displayName}
190 </Text>
191 ,{' '}
192 <Text key={slice[1].profile.did} style={textStyle}>
193 {slice[1].profile.displayName}
194 </Text>
195 , and{' '}
196 <Plural
197 value={serverCount - 2}
198 one="# other"
199 other="# others"
200 />
201 </Trans>
202 ) : (
203 // only 2
204 <Trans>
205 Followed by{' '}
206 <Text key={slice[0].profile.did} style={textStyle}>
207 {slice[0].profile.displayName}
208 </Text>{' '}
209 and{' '}
210 <Text key={slice[1].profile.did} style={textStyle}>
211 {slice[1].profile.displayName}
212 </Text>
213 </Trans>
214 )
215 ) : serverCount > 1 ? (
216 // 1-n followers, including blocks
217 <Trans>
218 Followed by{' '}
219 <Text key={slice[0].profile.did} style={textStyle}>
220 {slice[0].profile.displayName}
221 </Text>{' '}
222 and{' '}
223 <Plural
224 value={serverCount - 1}
225 one="# other"
226 other="# others"
227 />
228 </Trans>
229 ) : (
230 // only 1
231 <Trans>
232 Followed by{' '}
233 <Text key={slice[0].profile.did} style={textStyle}>
234 {slice[0].profile.displayName}
235 </Text>
236 </Trans>
237 )}
238 </Text>
239 </>
240 )}
241 </Link>
242 )
243}