Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
119
fork

Configure Feed

Select the types of activity you want to include in your feed.

at a876aae44ea07494ebea9727350aa060b81f317b 676 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} from '@lingui/core/macro' 15import {useLingui} from '@lingui/react' 16import {Trans} from '@lingui/react/macro' 17import {useQueryClient} from '@tanstack/react-query' 18 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 {convertCdnPreset} from '#/lib/media/util' 28import {makeProfileLink} from '#/lib/routes/links' 29import {sanitizeDisplayName} from '#/lib/strings/display-names' 30import {isCancelledError} from '#/lib/strings/errors' 31import {sanitizeHandle} from '#/lib/strings/handles' 32import {logger} from '#/logger' 33import { 34 type ComposerImage, 35 compressImage, 36 createComposerImage, 37} from '#/state/gallery' 38import {useEnableSquareAvatars} from '#/state/preferences/enable-square-avatars' 39import {useHighQualityImages} from '#/state/preferences/high-quality-images' 40import { 41 applyImageTransforms, 42 useImageCdnHost, 43} from '#/state/preferences/image-cdn-host' 44import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache' 45import {EditImageDialog} from '#/view/com/composer/photos/EditImageDialog' 46import {atoms as a, tokens, useTheme} from '#/alf' 47import {Button} from '#/components/Button' 48import {useDialogControl} from '#/components/Dialog' 49import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' 50import { 51 Camera_Filled_Stroke2_Corner0_Rounded as CameraFilledIcon, 52 Camera_Stroke2_Corner0_Rounded as CameraIcon, 53} from '#/components/icons/Camera' 54import {StreamingLive_Stroke2_Corner0_Rounded as LibraryIcon} from '#/components/icons/StreamingLive' 55import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 56import {Link} from '#/components/Link' 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 {useActorStatus} from '#/features/liveNow' 63import {LiveIndicator} from '#/features/liveNow/components/LiveIndicator' 64import {LiveStatusDialog} from '#/features/liveNow/components/LiveStatusDialog' 65import type * as bsky from '#/types/bsky' 66 67export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler' 68 69interface BaseUserAvatarProps { 70 type?: UserAvatarType 71 shape?: 'circle' | 'square' 72 size: number 73 avatar?: string | null 74 live?: boolean 75 hideLiveBadge?: boolean 76} 77 78interface UserAvatarProps extends BaseUserAvatarProps { 79 type: UserAvatarType 80 moderation?: ModerationUI 81 usePlainRNImage?: boolean 82 noBorder?: boolean 83 onLoad?: () => void 84 style?: StyleProp<ViewStyle> 85} 86 87interface EditableUserAvatarProps extends BaseUserAvatarProps { 88 onSelectNewAvatar: (img: PickerImage | null) => void 89} 90 91interface PreviewableUserAvatarProps extends BaseUserAvatarProps { 92 moderation?: ModerationUI 93 profile: bsky.profile.AnyProfileView 94 disableHoverCard?: boolean 95 disableNavigation?: boolean 96 onBeforePress?: () => void 97} 98 99const BLUR_AMOUNT = IS_WEB ? 5 : 100 100 101let DefaultAvatar = ({ 102 type, 103 shape: overrideShape, 104 size, 105}: { 106 type: UserAvatarType 107 shape?: 'square' | 'circle' 108 size: number 109}): React.ReactNode => { 110 const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square') 111 const t = useTheme() 112 113 const enableSquareAvatars = useEnableSquareAvatars() 114 115 const aviStyle = useMemo(() => { 116 if (finalShape === 'square') { 117 return {borderRadius: size > 32 ? 8 : 3, overflow: 'hidden'} as const 118 } 119 }, [finalShape, size]) 120 121 if (type === 'algo') { 122 // TODO: shape=circle 123 // Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. 124 return ( 125 <Svg 126 testID="userAvatarFallback" 127 width={size} 128 height={size} 129 viewBox="0 0 32 32" 130 fill="none" 131 stroke="none" 132 style={aviStyle}> 133 <Rect width="32" height="32" rx="4" fill={t.palette.primary_500} /> 134 <Path 135 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" 136 fill="white" 137 /> 138 </Svg> 139 ) 140 } 141 if (type === 'list') { 142 // TODO: shape=circle 143 // Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. 144 return ( 145 <Svg 146 testID="userAvatarFallback" 147 width={size} 148 height={size} 149 viewBox="0 0 32 32" 150 fill="none" 151 stroke="none" 152 style={aviStyle}> 153 <Path 154 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" 155 fill={t.palette.primary_500} 156 /> 157 <Path 158 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" 159 fill="white" 160 /> 161 <Path 162 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" 163 fill="white" 164 /> 165 </Svg> 166 ) 167 } 168 if (type === 'labeler') { 169 return ( 170 <Svg 171 testID="userAvatarFallback" 172 width={size} 173 height={size} 174 viewBox="0 0 32 32" 175 fill="none" 176 stroke="none" 177 style={aviStyle}> 178 {finalShape === 'square' ? ( 179 <Rect 180 x="0" 181 y="0" 182 width="32" 183 height="32" 184 rx="3" 185 fill={tokens.color.temp_purple} 186 /> 187 ) : ( 188 <Circle cx="16" cy="16" r="16" fill={tokens.color.temp_purple} /> 189 )} 190 <Path 191 d="M24 9.75L16 7L8 9.75V15.9123C8 20.8848 12 23 16 25.1579C20 23 24 20.8848 24 15.9123V9.75Z" 192 stroke="white" 193 strokeWidth="2" 194 strokeLinecap="square" 195 strokeLinejoin="round" 196 /> 197 </Svg> 198 ) 199 } 200 return ( 201 <Svg 202 testID="userAvatarFallback" 203 width={size} 204 height={size} 205 viewBox="0 0 24 24" 206 fill="none" 207 stroke="none" 208 style={aviStyle}> 209 {enableSquareAvatars ? ( 210 <Rect 211 x="0" 212 y="0" 213 width="24" 214 height="24" 215 rx="3" 216 fill={t.palette.primary_500} 217 /> 218 ) : ( 219 <Circle cx="12" cy="12" r="12" fill={t.palette.primary_500} /> 220 )} 221 <Circle cx="12" cy="9.5" r="3.5" fill="#fff" /> 222 <Path 223 strokeLinecap="round" 224 strokeLinejoin="round" 225 fill="#fff" 226 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" 227 /> 228 </Svg> 229 ) 230} 231DefaultAvatar = memo(DefaultAvatar) 232export {DefaultAvatar} 233 234let UserAvatar = ({ 235 type = 'user', 236 shape: overrideShape, 237 size, 238 avatar, 239 moderation, 240 usePlainRNImage = false, 241 onLoad, 242 style, 243 live, 244 hideLiveBadge, 245 noBorder, 246}: UserAvatarProps): React.ReactNode => { 247 const t = useTheme() 248 249 const enableSquareAvatars = useEnableSquareAvatars() 250 const avishapeforce = enableSquareAvatars ? 'square' : 'circle' 251 252 const finalShape = 253 overrideShape ?? (type === 'user' ? avishapeforce : 'square') 254 const highQualityImages = useHighQualityImages() 255 const imageCdnHost = useImageCdnHost() 256 257 const aviStyle = useMemo(() => { 258 let borderRadius 259 260 if (finalShape === 'square') { 261 borderRadius = size > 32 ? 8 : 3 262 } else { 263 borderRadius = Math.floor(size / 2) 264 } 265 266 return { 267 width: size, 268 height: size, 269 borderRadius, 270 backgroundColor: t.palette.contrast_25, 271 } 272 }, [finalShape, size, t]) 273 274 const borderStyle = useMemo(() => { 275 return [ 276 {borderRadius: aviStyle.borderRadius}, 277 live && { 278 borderColor: t.palette.negative_500, 279 borderWidth: size > 16 ? 2 : 1, 280 opacity: 1, 281 }, 282 ] 283 }, [aviStyle.borderRadius, live, t, size]) 284 285 const alert = useMemo(() => { 286 if (!moderation?.alert) { 287 return null 288 } 289 return ( 290 <View 291 style={[ 292 a.absolute, 293 a.right_0, 294 a.bottom_0, 295 a.rounded_full, 296 {backgroundColor: t.palette.white}, 297 ]}> 298 <FontAwesomeIcon 299 icon="exclamation-circle" 300 style={{color: t.palette.negative_400}} 301 size={Math.floor(size / 3)} 302 /> 303 </View> 304 ) 305 }, [moderation?.alert, size, t]) 306 307 const containerStyle = useMemo(() => { 308 return [ 309 { 310 width: size, 311 height: size, 312 }, 313 style, 314 ] 315 }, [size, style]) 316 317 return avatar && 318 !((moderation?.blur && IS_ANDROID) /* android crashes with blur */) ? ( 319 <View style={containerStyle}> 320 {usePlainRNImage ? ( 321 <RNImage 322 accessibilityIgnoresInvertColors 323 testID="userAvatarImage" 324 style={aviStyle} 325 resizeMode="cover" 326 source={{ 327 uri: applyImageTransforms( 328 hackModifyThumbnailPath(avatar, size < 90), 329 {imageCdnHost, highQualityImages}, 330 ), 331 }} 332 blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} 333 onLoad={onLoad} 334 /> 335 ) : ( 336 <ExpoImage 337 testID="userAvatarImage" 338 style={aviStyle} 339 contentFit="cover" 340 source={{ 341 uri: applyImageTransforms( 342 hackModifyThumbnailPath(avatar, size < 90), 343 {imageCdnHost, highQualityImages}, 344 ), 345 }} 346 blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} 347 onLoad={onLoad} 348 /> 349 )} 350 {!noBorder && <MediaInsetBorder style={borderStyle} />} 351 {live && size > 16 && !hideLiveBadge && ( 352 <LiveIndicator size={size > 32 ? 'small' : 'tiny'} /> 353 )} 354 {alert} 355 </View> 356 ) : ( 357 <View style={containerStyle}> 358 <DefaultAvatar type={type} shape={finalShape} size={size} /> 359 {!noBorder && <MediaInsetBorder style={borderStyle} />} 360 {live && size > 16 && !hideLiveBadge && ( 361 <LiveIndicator size={size > 32 ? 'small' : 'tiny'} /> 362 )} 363 {alert} 364 </View> 365 ) 366} 367UserAvatar = memo(UserAvatar) 368export {UserAvatar} 369 370let EditableUserAvatar = ({ 371 type = 'user', 372 size, 373 avatar, 374 onSelectNewAvatar, 375}: EditableUserAvatarProps): React.ReactNode => { 376 const t = useTheme() 377 const {_} = useLingui() 378 const {requestCameraAccessIfNeeded} = useCameraPermission() 379 const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() 380 const [rawImage, setRawImage] = useState<ComposerImage | undefined>() 381 const editImageDialogControl = useDialogControl() 382 383 const sheetWrapper = useSheetWrapper() 384 const highQualityImages = useHighQualityImages() 385 const imageCdnHost = useImageCdnHost() 386 387 const enableSquareAvatars = useEnableSquareAvatars() 388 389 const circular = type !== 'algo' && type !== 'list' && !enableSquareAvatars 390 391 const aviStyle = useMemo(() => { 392 if (!circular) { 393 return { 394 width: size, 395 height: size, 396 borderRadius: size > 32 ? 8 : 3, 397 } 398 } 399 return { 400 width: size, 401 height: size, 402 borderRadius: Math.floor(size / 2), 403 } 404 }, [circular, size]) 405 406 const onOpenCamera = useCallback(async () => { 407 if (!(await requestCameraAccessIfNeeded())) { 408 return 409 } 410 411 onSelectNewAvatar( 412 await compressIfNeeded( 413 await openCamera({ 414 aspect: [1, 1], 415 }), 416 ), 417 ) 418 }, [onSelectNewAvatar, requestCameraAccessIfNeeded]) 419 420 const onOpenLibrary = useCallback(async () => { 421 if (!(await requestPhotoAccessIfNeeded())) { 422 return 423 } 424 425 const items = await sheetWrapper( 426 openPicker({ 427 aspect: [1, 1], 428 }), 429 ) 430 const item = items[0] 431 if (!item) { 432 return 433 } 434 435 try { 436 if (IS_NATIVE) { 437 onSelectNewAvatar( 438 await compressIfNeeded( 439 await openCropper({ 440 imageUri: item.path, 441 shape: circular ? 'circle' : 'rectangle', 442 aspectRatio: 1, 443 }), 444 ), 445 ) 446 } else { 447 setRawImage(await createComposerImage(item)) 448 editImageDialogControl.open() 449 } 450 } catch (e) { 451 // Don't log errors for cancelling selection to sentry on ios or android 452 if (!isCancelledError(e)) { 453 logger.error('Failed to crop avatar', {error: e}) 454 } 455 } 456 }, [ 457 onSelectNewAvatar, 458 requestPhotoAccessIfNeeded, 459 sheetWrapper, 460 editImageDialogControl, 461 circular, 462 ]) 463 464 const onRemoveAvatar = useCallback(() => { 465 onSelectNewAvatar(null) 466 }, [onSelectNewAvatar]) 467 468 const onChangeEditImage = useCallback( 469 async (image: ComposerImage) => { 470 const compressed = await compressImage(image) 471 onSelectNewAvatar(compressed) 472 }, 473 [onSelectNewAvatar], 474 ) 475 476 return ( 477 <> 478 <Menu.Root> 479 <Menu.Trigger label={_(msg`Edit avatar`)}> 480 {({props}) => ( 481 <Pressable {...props} testID="changeAvatarBtn"> 482 {avatar ? ( 483 <ExpoImage 484 testID="userAvatarImage" 485 style={aviStyle} 486 source={{ 487 uri: applyImageTransforms(avatar, { 488 imageCdnHost, 489 highQualityImages, 490 }), 491 }} 492 accessibilityRole="image" 493 /> 494 ) : ( 495 <DefaultAvatar type={type} size={size} /> 496 )} 497 <View 498 style={[ 499 styles.editButtonContainer, 500 t.atoms.bg_contrast_25, 501 a.border, 502 t.atoms.border_contrast_low, 503 ]}> 504 <CameraFilledIcon height={14} width={14} style={t.atoms.text} /> 505 </View> 506 </Pressable> 507 )} 508 </Menu.Trigger> 509 <Menu.Outer showCancel> 510 <Menu.Group> 511 {IS_NATIVE && ( 512 <Menu.Item 513 testID="changeAvatarCameraBtn" 514 label={_(msg`Upload from Camera`)} 515 onPress={onOpenCamera}> 516 <Menu.ItemText> 517 <Trans>Upload from Camera</Trans> 518 </Menu.ItemText> 519 <Menu.ItemIcon icon={CameraIcon} /> 520 </Menu.Item> 521 )} 522 523 <Menu.Item 524 testID="changeAvatarLibraryBtn" 525 label={_(msg`Upload from Library`)} 526 onPress={onOpenLibrary}> 527 <Menu.ItemText> 528 {IS_NATIVE ? ( 529 <Trans>Upload from Library</Trans> 530 ) : ( 531 <Trans>Upload from Files</Trans> 532 )} 533 </Menu.ItemText> 534 <Menu.ItemIcon icon={LibraryIcon} /> 535 </Menu.Item> 536 </Menu.Group> 537 {!!avatar && ( 538 <> 539 <Menu.Divider /> 540 <Menu.Group> 541 <Menu.Item 542 testID="changeAvatarRemoveBtn" 543 label={_(msg`Remove Avatar`)} 544 onPress={onRemoveAvatar}> 545 <Menu.ItemText> 546 <Trans>Remove Avatar</Trans> 547 </Menu.ItemText> 548 <Menu.ItemIcon icon={TrashIcon} /> 549 </Menu.Item> 550 </Menu.Group> 551 </> 552 )} 553 </Menu.Outer> 554 </Menu.Root> 555 556 <EditImageDialog 557 control={editImageDialogControl} 558 image={rawImage} 559 onChange={onChangeEditImage} 560 aspectRatio={1} 561 circularCrop={circular} 562 /> 563 </> 564 ) 565} 566EditableUserAvatar = memo(EditableUserAvatar) 567export {EditableUserAvatar} 568 569let PreviewableUserAvatar = ({ 570 moderation, 571 profile, 572 disableHoverCard, 573 disableNavigation, 574 onBeforePress, 575 live, 576 ...props 577}: PreviewableUserAvatarProps): React.ReactNode => { 578 const ax = useAnalytics() 579 const {_} = useLingui() 580 const queryClient = useQueryClient() 581 const status = useActorStatus(profile) 582 const liveControl = useDialogControl() 583 const playHaptic = useHaptics() 584 585 const onPress = useCallback(() => { 586 onBeforePress?.() 587 unstableCacheProfileView(queryClient, profile) 588 }, [profile, queryClient, onBeforePress]) 589 590 const onOpenLiveStatus = useCallback(() => { 591 playHaptic('Light') 592 ax.metric('live:card:open', {subject: profile.did, from: 'post'}) 593 liveControl.open() 594 }, [liveControl, playHaptic, profile.did]) 595 596 const avatarEl = ( 597 <UserAvatar 598 avatar={profile.avatar} 599 moderation={moderation} 600 type={profile.associated?.labeler ? 'labeler' : 'user'} 601 live={status.isActive || live} 602 {...props} 603 /> 604 ) 605 606 const linkStyle = 607 props.type !== 'algo' && props.type !== 'list' 608 ? a.rounded_full 609 : {borderRadius: props.size > 32 ? 8 : 3} 610 611 return ( 612 <ProfileHoverCard did={profile.did} disable={disableHoverCard}> 613 {disableNavigation ? ( 614 avatarEl 615 ) : status.isActive && (IS_NATIVE || IS_WEB_TOUCH_DEVICE) ? ( 616 <> 617 <Button 618 label={_( 619 msg`${sanitizeDisplayName( 620 profile.displayName || sanitizeHandle(profile.handle), 621 )}'s avatar`, 622 )} 623 accessibilityHint={_(msg`Opens live status dialog`)} 624 onPress={onOpenLiveStatus}> 625 {avatarEl} 626 </Button> 627 <LiveStatusDialog 628 control={liveControl} 629 profile={profile} 630 status={status} 631 embed={status.embed} 632 /> 633 </> 634 ) : ( 635 <Link 636 label={_( 637 msg`${sanitizeDisplayName( 638 profile.displayName || sanitizeHandle(profile.handle), 639 )}'s avatar`, 640 )} 641 accessibilityHint={_(msg`Opens this profile`)} 642 to={makeProfileLink({ 643 did: profile.did, 644 handle: profile.handle, 645 })} 646 onPress={onPress} 647 style={linkStyle}> 648 {avatarEl} 649 </Link> 650 )} 651 </ProfileHoverCard> 652 ) 653} 654PreviewableUserAvatar = memo(PreviewableUserAvatar) 655export {PreviewableUserAvatar} 656 657// HACK 658// We have started serving smaller avis but haven't updated lexicons to give the data properly 659// manually string-replace to use the smaller ones 660// -prf 661function hackModifyThumbnailPath(uri: string, isEnabled: boolean): string { 662 return isEnabled ? convertCdnPreset(uri, 'avatar_thumbnail') : uri 663} 664 665const styles = StyleSheet.create({ 666 editButtonContainer: { 667 position: 'absolute', 668 width: 24, 669 height: 24, 670 bottom: 0, 671 right: 0, 672 borderRadius: 12, 673 alignItems: 'center', 674 justifyContent: 'center', 675 }, 676})