forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {memo, useCallback, useMemo, useState} from 'react'
2import {
3 Image as RNImage,
4 Pressable,
5 type StyleProp,
6 StyleSheet,
7 View,
8 type ViewStyle,
9} from 'react-native'
10import Svg, {Circle, Path, Rect} from 'react-native-svg'
11import {Image as ExpoImage} from 'expo-image'
12import {type ModerationUI} from '@atproto/api'
13import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
14import {msg, Trans} from '@lingui/macro'
15import {useLingui} from '@lingui/react'
16import {useQueryClient} from '@tanstack/react-query'
17
18import {useActorStatus} from '#/lib/actor-status'
19import {useHaptics} from '#/lib/haptics'
20import {
21 useCameraPermission,
22 usePhotoLibraryPermission,
23} from '#/lib/hooks/usePermissions'
24import {compressIfNeeded} from '#/lib/media/manip'
25import {openCamera, openCropper, openPicker} from '#/lib/media/picker'
26import {type PickerImage} from '#/lib/media/picker.shared'
27import {makeProfileLink} from '#/lib/routes/links'
28import {sanitizeDisplayName} from '#/lib/strings/display-names'
29import {isCancelledError} from '#/lib/strings/errors'
30import {sanitizeHandle} from '#/lib/strings/handles'
31import {logger} from '#/logger'
32import {
33 type ComposerImage,
34 compressImage,
35 createComposerImage,
36} from '#/state/gallery'
37import {useEnableSquareAvatars} from '#/state/preferences/enable-square-avatars'
38import {
39 maybeModifyHighQualityImage,
40 useHighQualityImages,
41} from '#/state/preferences/high-quality-images'
42import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache'
43import {EditImageDialog} from '#/view/com/composer/photos/EditImageDialog'
44import {atoms as a, tokens, useTheme} from '#/alf'
45import {Button} from '#/components/Button'
46import {useDialogControl} from '#/components/Dialog'
47import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
48import {
49 Camera_Filled_Stroke2_Corner0_Rounded as CameraFilledIcon,
50 Camera_Stroke2_Corner0_Rounded as CameraIcon,
51} from '#/components/icons/Camera'
52import {StreamingLive_Stroke2_Corner0_Rounded as LibraryIcon} from '#/components/icons/StreamingLive'
53import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
54import {Link} from '#/components/Link'
55import {LiveIndicator} from '#/components/live/LiveIndicator'
56import {LiveStatusDialog} from '#/components/live/LiveStatusDialog'
57import {MediaInsetBorder} from '#/components/MediaInsetBorder'
58import * as Menu from '#/components/Menu'
59import {ProfileHoverCard} from '#/components/ProfileHoverCard'
60import {useAnalytics} from '#/analytics'
61import {IS_ANDROID, IS_NATIVE, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
62import type * as bsky from '#/types/bsky'
63
64export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler'
65
66interface BaseUserAvatarProps {
67 type?: UserAvatarType
68 shape?: 'circle' | 'square'
69 size: number
70 avatar?: string | null
71 live?: boolean
72 hideLiveBadge?: boolean
73}
74
75interface UserAvatarProps extends BaseUserAvatarProps {
76 type: UserAvatarType
77 moderation?: ModerationUI
78 usePlainRNImage?: boolean
79 noBorder?: boolean
80 onLoad?: () => void
81 style?: StyleProp<ViewStyle>
82}
83
84interface EditableUserAvatarProps extends BaseUserAvatarProps {
85 onSelectNewAvatar: (img: PickerImage | null) => void
86}
87
88interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
89 moderation?: ModerationUI
90 profile: bsky.profile.AnyProfileView
91 disableHoverCard?: boolean
92 disableNavigation?: boolean
93 onBeforePress?: () => void
94}
95
96const BLUR_AMOUNT = IS_WEB ? 5 : 100
97
98let DefaultAvatar = ({
99 type,
100 shape: overrideShape,
101 size,
102}: {
103 type: UserAvatarType
104 shape?: 'square' | 'circle'
105 size: number
106}): React.ReactNode => {
107 const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square')
108 const t = useTheme()
109
110 const enableSquareAvatars = useEnableSquareAvatars()
111
112 const aviStyle = useMemo(() => {
113 if (finalShape === 'square') {
114 return {borderRadius: size > 32 ? 8 : 3, overflow: 'hidden'} as const
115 }
116 }, [finalShape, size])
117
118 if (type === 'algo') {
119 // TODO: shape=circle
120 // Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc.
121 return (
122 <Svg
123 testID="userAvatarFallback"
124 width={size}
125 height={size}
126 viewBox="0 0 32 32"
127 fill="none"
128 stroke="none"
129 style={aviStyle}>
130 <Rect width="32" height="32" rx="4" fill={t.palette.primary_500} />
131 <Path
132 d="M13.5 7.25C13.5 6.55859 14.0586 6 14.75 6C20.9648 6 26 11.0352 26 17.25C26 17.9414 25.4414 18.5 24.75 18.5C24.0586 18.5 23.5 17.9414 23.5 17.25C23.5 12.418 19.582 8.5 14.75 8.5C14.0586 8.5 13.5 7.94141 13.5 7.25ZM8.36719 14.6172L12.4336 18.6836L13.543 17.5742C13.5156 17.4727 13.5 17.3633 13.5 17.25C13.5 16.5586 14.0586 16 14.75 16C15.4414 16 16 16.5586 16 17.25C16 17.9414 15.4414 18.5 14.75 18.5C14.6367 18.5 14.5312 18.4844 14.4258 18.457L13.3164 19.5664L17.3828 23.6328C17.9492 24.1992 17.8438 25.1484 17.0977 25.4414C16.1758 25.8008 15.1758 26 14.125 26C9.63672 26 6 22.3633 6 17.875C6 16.8242 6.19922 15.8242 6.5625 14.9023C6.85547 14.1602 7.80469 14.0508 8.37109 14.6172H8.36719ZM14.75 9.75C18.8906 9.75 22.25 13.1094 22.25 17.25C22.25 17.9414 21.6914 18.5 21 18.5C20.3086 18.5 19.75 17.9414 19.75 17.25C19.75 14.4883 17.5117 12.25 14.75 12.25C14.0586 12.25 13.5 11.6914 13.5 11C13.5 10.3086 14.0586 9.75 14.75 9.75Z"
133 fill="white"
134 />
135 </Svg>
136 )
137 }
138 if (type === 'list') {
139 // TODO: shape=circle
140 // Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc.
141 return (
142 <Svg
143 testID="userAvatarFallback"
144 width={size}
145 height={size}
146 viewBox="0 0 32 32"
147 fill="none"
148 stroke="none"
149 style={aviStyle}>
150 <Path
151 d="M28 0H4C1.79086 0 0 1.79086 0 4V28C0 30.2091 1.79086 32 4 32H28C30.2091 32 32 30.2091 32 28V4C32 1.79086 30.2091 0 28 0Z"
152 fill={t.palette.primary_500}
153 />
154 <Path
155 d="M22.1529 22.3542C23.4522 22.4603 24.7593 22.293 25.9899 21.8629C26.0369 21.2838 25.919 20.7032 25.6497 20.1884C25.3805 19.6735 24.9711 19.2454 24.4687 18.9535C23.9663 18.6617 23.3916 18.518 22.8109 18.5392C22.2303 18.5603 21.6676 18.7454 21.1878 19.0731M22.1529 22.3542C22.1489 21.1917 21.8142 20.0534 21.1878 19.0741ZM10.8111 19.0741C10.3313 18.7468 9.7687 18.5619 9.18826 18.5409C8.60781 18.5199 8.03327 18.6636 7.53107 18.9554C7.02888 19.2472 6.61953 19.6752 6.35036 20.1899C6.08119 20.7046 5.96319 21.285 6.01001 21.8639C7.23969 22.2964 8.5461 22.4632 9.84497 22.3531M10.8111 19.0741C10.1851 20.0535 9.84865 21.1908 9.84497 22.3531ZM19.0759 10.077C19.0759 10.8931 18.7518 11.6757 18.1747 12.2527C17.5977 12.8298 16.815 13.154 15.9989 13.154C15.1829 13.154 14.4002 12.8298 13.8232 12.2527C13.2461 11.6757 12.922 10.8931 12.922 10.077C12.922 9.26092 13.2461 8.47828 13.8232 7.90123C14.4002 7.32418 15.1829 7 15.9989 7C16.815 7 17.5977 7.32418 18.1747 7.90123C18.7518 8.47828 19.0759 9.26092 19.0759 10.077ZM25.2299 13.154C25.2299 13.457 25.1702 13.7571 25.0542 14.0371C24.9383 14.3171 24.7683 14.5715 24.554 14.7858C24.3397 15.0001 24.0853 15.1701 23.8053 15.2861C23.5253 15.402 23.2252 15.4617 22.9222 15.4617C22.6191 15.4617 22.319 15.402 22.039 15.2861C21.759 15.1701 21.5046 15.0001 21.2903 14.7858C21.0761 14.5715 20.9061 14.3171 20.7901 14.0371C20.6741 13.7571 20.6144 13.457 20.6144 13.154C20.6144 12.5419 20.8576 11.9549 21.2903 11.5222C21.7231 11.0894 22.3101 10.8462 22.9222 10.8462C23.5342 10.8462 24.1212 11.0894 24.554 11.5222C24.9868 11.9549 25.2299 12.5419 25.2299 13.154ZM11.3835 13.154C11.3835 13.457 11.3238 13.7571 11.2078 14.0371C11.0918 14.3171 10.9218 14.5715 10.7075 14.7858C10.4932 15.0001 10.2388 15.1701 9.95886 15.2861C9.67887 15.402 9.37878 15.4617 9.07572 15.4617C8.77266 15.4617 8.47257 15.402 8.19259 15.2861C7.9126 15.1701 7.6582 15.0001 7.4439 14.7858C7.22961 14.5715 7.05962 14.3171 6.94365 14.0371C6.82767 13.7571 6.76798 13.457 6.76798 13.154C6.76798 12.5419 7.01112 11.9549 7.4439 11.5222C7.87669 11.0894 8.46367 10.8462 9.07572 10.8462C9.68777 10.8462 10.2748 11.0894 10.7075 11.5222C11.1403 11.9549 11.3835 12.5419 11.3835 13.154Z"
156 fill="white"
157 />
158 <Path
159 d="M22 22C22 25.3137 19.3137 25.5 16 25.5C12.6863 25.5 10 25.3137 10 22C10 18.6863 12.6863 16 16 16C19.3137 16 22 18.6863 22 22Z"
160 fill="white"
161 />
162 </Svg>
163 )
164 }
165 if (type === 'labeler') {
166 return (
167 <Svg
168 testID="userAvatarFallback"
169 width={size}
170 height={size}
171 viewBox="0 0 32 32"
172 fill="none"
173 stroke="none"
174 style={aviStyle}>
175 {finalShape === 'square' ? (
176 <Rect
177 x="0"
178 y="0"
179 width="32"
180 height="32"
181 rx="3"
182 fill={tokens.color.temp_purple}
183 />
184 ) : (
185 <Circle cx="16" cy="16" r="16" fill={tokens.color.temp_purple} />
186 )}
187 <Path
188 d="M24 9.75L16 7L8 9.75V15.9123C8 20.8848 12 23 16 25.1579C20 23 24 20.8848 24 15.9123V9.75Z"
189 stroke="white"
190 strokeWidth="2"
191 strokeLinecap="square"
192 strokeLinejoin="round"
193 />
194 </Svg>
195 )
196 }
197 return (
198 <Svg
199 testID="userAvatarFallback"
200 width={size}
201 height={size}
202 viewBox="0 0 24 24"
203 fill="none"
204 stroke="none"
205 style={aviStyle}>
206 {enableSquareAvatars ? (
207 <Rect
208 x="0"
209 y="0"
210 width="24"
211 height="24"
212 rx="3"
213 fill={t.palette.primary_500}
214 />
215 ) : (
216 <Circle cx="12" cy="12" r="12" fill={t.palette.primary_500} />
217 )}
218 <Circle cx="12" cy="9.5" r="3.5" fill="#fff" />
219 <Path
220 strokeLinecap="round"
221 strokeLinejoin="round"
222 fill="#fff"
223 d="M 12.058 22.784 C 9.422 22.784 7.007 21.836 5.137 20.262 C 5.667 17.988 8.534 16.25 11.99 16.25 C 15.494 16.25 18.391 18.036 18.864 20.357 C 17.01 21.874 14.64 22.784 12.058 22.784 Z"
224 />
225 </Svg>
226 )
227}
228DefaultAvatar = memo(DefaultAvatar)
229export {DefaultAvatar}
230
231let UserAvatar = ({
232 type = 'user',
233 shape: overrideShape,
234 size,
235 avatar,
236 moderation,
237 usePlainRNImage = false,
238 onLoad,
239 style,
240 live,
241 hideLiveBadge,
242 noBorder,
243}: UserAvatarProps): React.ReactNode => {
244 const t = useTheme()
245
246 const enableSquareAvatars = useEnableSquareAvatars()
247 const avishapeforce = enableSquareAvatars ? 'square' : 'circle'
248
249 const finalShape =
250 overrideShape ?? (type === 'user' ? avishapeforce : 'square')
251 const highQualityImages = useHighQualityImages()
252
253 const aviStyle = useMemo(() => {
254 let borderRadius
255
256 if (finalShape === 'square') {
257 borderRadius = size > 32 ? 8 : 3
258 } else {
259 borderRadius = Math.floor(size / 2)
260 }
261
262 return {
263 width: size,
264 height: size,
265 borderRadius,
266 backgroundColor: t.palette.contrast_25,
267 }
268 }, [finalShape, size, t])
269
270 const borderStyle = useMemo(() => {
271 return [
272 {borderRadius: aviStyle.borderRadius},
273 live && {
274 borderColor: t.palette.negative_500,
275 borderWidth: size > 16 ? 2 : 1,
276 opacity: 1,
277 },
278 ]
279 }, [aviStyle.borderRadius, live, t, size])
280
281 const alert = useMemo(() => {
282 if (!moderation?.alert) {
283 return null
284 }
285 return (
286 <View
287 style={[
288 a.absolute,
289 a.right_0,
290 a.bottom_0,
291 a.rounded_full,
292 {backgroundColor: t.palette.white},
293 ]}>
294 <FontAwesomeIcon
295 icon="exclamation-circle"
296 style={{color: t.palette.negative_400}}
297 size={Math.floor(size / 3)}
298 />
299 </View>
300 )
301 }, [moderation?.alert, size, t])
302
303 const containerStyle = useMemo(() => {
304 return [
305 {
306 width: size,
307 height: size,
308 },
309 style,
310 ]
311 }, [size, style])
312
313 return avatar &&
314 !((moderation?.blur && IS_ANDROID) /* android crashes with blur */) ? (
315 <View style={containerStyle}>
316 {usePlainRNImage ? (
317 <RNImage
318 accessibilityIgnoresInvertColors
319 testID="userAvatarImage"
320 style={aviStyle}
321 resizeMode="cover"
322 source={{
323 uri: maybeModifyHighQualityImage(
324 hackModifyThumbnailPath(avatar, size < 90),
325 highQualityImages,
326 ),
327 }}
328 blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
329 onLoad={onLoad}
330 />
331 ) : (
332 <ExpoImage
333 testID="userAvatarImage"
334 style={aviStyle}
335 contentFit="cover"
336 source={{
337 uri: maybeModifyHighQualityImage(
338 hackModifyThumbnailPath(avatar, size < 90),
339 highQualityImages,
340 ),
341 }}
342 blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
343 onLoad={onLoad}
344 />
345 )}
346 {!noBorder && <MediaInsetBorder style={borderStyle} />}
347 {live && size > 16 && !hideLiveBadge && (
348 <LiveIndicator size={size > 32 ? 'small' : 'tiny'} />
349 )}
350 {alert}
351 </View>
352 ) : (
353 <View style={containerStyle}>
354 <DefaultAvatar type={type} shape={finalShape} size={size} />
355 {!noBorder && <MediaInsetBorder style={borderStyle} />}
356 {live && size > 16 && !hideLiveBadge && (
357 <LiveIndicator size={size > 32 ? 'small' : 'tiny'} />
358 )}
359 {alert}
360 </View>
361 )
362}
363UserAvatar = memo(UserAvatar)
364export {UserAvatar}
365
366let EditableUserAvatar = ({
367 type = 'user',
368 size,
369 avatar,
370 onSelectNewAvatar,
371}: EditableUserAvatarProps): React.ReactNode => {
372 const t = useTheme()
373 const {_} = useLingui()
374 const {requestCameraAccessIfNeeded} = useCameraPermission()
375 const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
376 const [rawImage, setRawImage] = useState<ComposerImage | undefined>()
377 const editImageDialogControl = useDialogControl()
378
379 const sheetWrapper = useSheetWrapper()
380 const highQualityImages = useHighQualityImages()
381
382 const enableSquareAvatars = useEnableSquareAvatars()
383
384 const circular = type !== 'algo' && type !== 'list' && !enableSquareAvatars
385
386 const aviStyle = useMemo(() => {
387 if (!circular) {
388 return {
389 width: size,
390 height: size,
391 borderRadius: size > 32 ? 8 : 3,
392 }
393 }
394 return {
395 width: size,
396 height: size,
397 borderRadius: Math.floor(size / 2),
398 }
399 }, [circular, size])
400
401 const onOpenCamera = useCallback(async () => {
402 if (!(await requestCameraAccessIfNeeded())) {
403 return
404 }
405
406 onSelectNewAvatar(
407 await compressIfNeeded(
408 await openCamera({
409 aspect: [1, 1],
410 }),
411 ),
412 )
413 }, [onSelectNewAvatar, requestCameraAccessIfNeeded])
414
415 const onOpenLibrary = useCallback(async () => {
416 if (!(await requestPhotoAccessIfNeeded())) {
417 return
418 }
419
420 const items = await sheetWrapper(
421 openPicker({
422 aspect: [1, 1],
423 }),
424 )
425 const item = items[0]
426 if (!item) {
427 return
428 }
429
430 try {
431 if (IS_NATIVE) {
432 onSelectNewAvatar(
433 await compressIfNeeded(
434 await openCropper({
435 imageUri: item.path,
436 shape: circular ? 'circle' : 'rectangle',
437 aspectRatio: 1,
438 }),
439 ),
440 )
441 } else {
442 setRawImage(await createComposerImage(item))
443 editImageDialogControl.open()
444 }
445 } catch (e) {
446 // Don't log errors for cancelling selection to sentry on ios or android
447 if (!isCancelledError(e)) {
448 logger.error('Failed to crop avatar', {error: e})
449 }
450 }
451 }, [
452 onSelectNewAvatar,
453 requestPhotoAccessIfNeeded,
454 sheetWrapper,
455 editImageDialogControl,
456 circular,
457 ])
458
459 const onRemoveAvatar = useCallback(() => {
460 onSelectNewAvatar(null)
461 }, [onSelectNewAvatar])
462
463 const onChangeEditImage = useCallback(
464 async (image: ComposerImage) => {
465 const compressed = await compressImage(image)
466 onSelectNewAvatar(compressed)
467 },
468 [onSelectNewAvatar],
469 )
470
471 return (
472 <>
473 <Menu.Root>
474 <Menu.Trigger label={_(msg`Edit avatar`)}>
475 {({props}) => (
476 <Pressable {...props} testID="changeAvatarBtn">
477 {avatar ? (
478 <ExpoImage
479 testID="userAvatarImage"
480 style={aviStyle}
481 source={{
482 uri: maybeModifyHighQualityImage(avatar, highQualityImages),
483 }}
484 accessibilityRole="image"
485 />
486 ) : (
487 <DefaultAvatar type={type} size={size} />
488 )}
489 <View
490 style={[
491 styles.editButtonContainer,
492 t.atoms.bg_contrast_25,
493 a.border,
494 t.atoms.border_contrast_low,
495 ]}>
496 <CameraFilledIcon height={14} width={14} style={t.atoms.text} />
497 </View>
498 </Pressable>
499 )}
500 </Menu.Trigger>
501 <Menu.Outer showCancel>
502 <Menu.Group>
503 {IS_NATIVE && (
504 <Menu.Item
505 testID="changeAvatarCameraBtn"
506 label={_(msg`Upload from Camera`)}
507 onPress={onOpenCamera}>
508 <Menu.ItemText>
509 <Trans>Upload from Camera</Trans>
510 </Menu.ItemText>
511 <Menu.ItemIcon icon={CameraIcon} />
512 </Menu.Item>
513 )}
514
515 <Menu.Item
516 testID="changeAvatarLibraryBtn"
517 label={_(msg`Upload from Library`)}
518 onPress={onOpenLibrary}>
519 <Menu.ItemText>
520 {IS_NATIVE ? (
521 <Trans>Upload from Library</Trans>
522 ) : (
523 <Trans>Upload from Files</Trans>
524 )}
525 </Menu.ItemText>
526 <Menu.ItemIcon icon={LibraryIcon} />
527 </Menu.Item>
528 </Menu.Group>
529 {!!avatar && (
530 <>
531 <Menu.Divider />
532 <Menu.Group>
533 <Menu.Item
534 testID="changeAvatarRemoveBtn"
535 label={_(msg`Remove Avatar`)}
536 onPress={onRemoveAvatar}>
537 <Menu.ItemText>
538 <Trans>Remove Avatar</Trans>
539 </Menu.ItemText>
540 <Menu.ItemIcon icon={TrashIcon} />
541 </Menu.Item>
542 </Menu.Group>
543 </>
544 )}
545 </Menu.Outer>
546 </Menu.Root>
547
548 <EditImageDialog
549 control={editImageDialogControl}
550 image={rawImage}
551 onChange={onChangeEditImage}
552 aspectRatio={1}
553 circularCrop={circular}
554 />
555 </>
556 )
557}
558EditableUserAvatar = memo(EditableUserAvatar)
559export {EditableUserAvatar}
560
561let PreviewableUserAvatar = ({
562 moderation,
563 profile,
564 disableHoverCard,
565 disableNavigation,
566 onBeforePress,
567 live,
568 ...props
569}: PreviewableUserAvatarProps): React.ReactNode => {
570 const ax = useAnalytics()
571 const {_} = useLingui()
572 const queryClient = useQueryClient()
573 const status = useActorStatus(profile)
574 const liveControl = useDialogControl()
575 const playHaptic = useHaptics()
576
577 const onPress = useCallback(() => {
578 onBeforePress?.()
579 unstableCacheProfileView(queryClient, profile)
580 }, [profile, queryClient, onBeforePress])
581
582 const onOpenLiveStatus = useCallback(() => {
583 playHaptic('Light')
584 ax.metric('live:card:open', {subject: profile.did, from: 'post'})
585 liveControl.open()
586 }, [liveControl, playHaptic, profile.did])
587
588 const avatarEl = (
589 <UserAvatar
590 avatar={profile.avatar}
591 moderation={moderation}
592 type={profile.associated?.labeler ? 'labeler' : 'user'}
593 live={status.isActive || live}
594 {...props}
595 />
596 )
597
598 const linkStyle =
599 props.type !== 'algo' && props.type !== 'list'
600 ? a.rounded_full
601 : {borderRadius: props.size > 32 ? 8 : 3}
602
603 return (
604 <ProfileHoverCard did={profile.did} disable={disableHoverCard}>
605 {disableNavigation ? (
606 avatarEl
607 ) : status.isActive && (IS_NATIVE || IS_WEB_TOUCH_DEVICE) ? (
608 <>
609 <Button
610 label={_(
611 msg`${sanitizeDisplayName(
612 profile.displayName || sanitizeHandle(profile.handle),
613 )}'s avatar`,
614 )}
615 accessibilityHint={_(msg`Opens live status dialog`)}
616 onPress={onOpenLiveStatus}>
617 {avatarEl}
618 </Button>
619 <LiveStatusDialog
620 control={liveControl}
621 profile={profile}
622 status={status}
623 embed={status.embed}
624 />
625 </>
626 ) : (
627 <Link
628 label={_(
629 msg`${sanitizeDisplayName(
630 profile.displayName || sanitizeHandle(profile.handle),
631 )}'s avatar`,
632 )}
633 accessibilityHint={_(msg`Opens this profile`)}
634 to={makeProfileLink({
635 did: profile.did,
636 handle: profile.handle,
637 })}
638 onPress={onPress}
639 style={linkStyle}>
640 {avatarEl}
641 </Link>
642 )}
643 </ProfileHoverCard>
644 )
645}
646PreviewableUserAvatar = memo(PreviewableUserAvatar)
647export {PreviewableUserAvatar}
648
649// HACK
650// We have started serving smaller avis but haven't updated lexicons to give the data properly
651// manually string-replace to use the smaller ones
652// -prf
653function hackModifyThumbnailPath(uri: string, isEnabled: boolean): string {
654 return isEnabled
655 ? uri.replace('/img/avatar/plain/', '/img/avatar_thumbnail/plain/')
656 : uri
657}
658
659const styles = StyleSheet.create({
660 editButtonContainer: {
661 position: 'absolute',
662 width: 24,
663 height: 24,
664 bottom: 0,
665 right: 0,
666 borderRadius: 12,
667 alignItems: 'center',
668 justifyContent: 'center',
669 },
670})