Bluesky app fork with some witchin' additions 馃挮
at readme-update 670 lines 22 kB view raw
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})