diff options
Diffstat (limited to 'src/view/com/util/UserAvatar.tsx')
-rw-r--r-- | src/view/com/util/UserAvatar.tsx | 88 |
1 files changed, 75 insertions, 13 deletions
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 326a2fff8..b3bf144f7 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -14,6 +14,9 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' +import {useActorStatus} from '#/lib/actor-status' +import {isTouchDevice} from '#/lib/browser' +import {useHaptics} from '#/lib/haptics' import { useCameraPermission, usePhotoLibraryPermission, @@ -22,6 +25,8 @@ import {compressIfNeeded} from '#/lib/media/manip' import {openCamera, openCropper, openPicker} from '#/lib/media/picker' import {type PickerImage} from '#/lib/media/picker.shared' import {makeProfileLink} from '#/lib/routes/links' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' import {logger} from '#/logger' import {isAndroid, isNative, isWeb} from '#/platform/detection' import { @@ -33,6 +38,7 @@ import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache' import {EditImageDialog} from '#/view/com/composer/photos/EditImageDialog' import {HighPriorityImage} from '#/view/com/util/images/Image' import {atoms as a, tokens, useTheme} from '#/alf' +import {Button} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' import { @@ -42,6 +48,8 @@ import { import {StreamingLive_Stroke2_Corner0_Rounded as LibraryIcon} from '#/components/icons/StreamingLive' import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' import {Link} from '#/components/Link' +import {LiveIndicator} from '#/components/live/LiveIndicator' +import {LiveStatusDialog} from '#/components/live/LiveStatusDialog' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import * as Menu from '#/components/Menu' import {ProfileHoverCard} from '#/components/ProfileHoverCard' @@ -54,6 +62,8 @@ interface BaseUserAvatarProps { shape?: 'circle' | 'square' size: number avatar?: string | null + live?: boolean + hideLiveBadge?: boolean } interface UserAvatarProps extends BaseUserAvatarProps { @@ -196,27 +206,38 @@ let UserAvatar = ({ usePlainRNImage = false, onLoad, style, + live, + hideLiveBadge, }: UserAvatarProps): React.ReactNode => { const t = useTheme() - const backgroundColor = t.palette.contrast_25 const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square') const aviStyle = useMemo(() => { + let borderRadius if (finalShape === 'square') { - return { - width: size, - height: size, - borderRadius: size > 32 ? 8 : 3, - backgroundColor, - } + borderRadius = size > 32 ? 8 : 3 + } else { + borderRadius = Math.floor(size / 2) } + return { width: size, height: size, - borderRadius: Math.floor(size / 2), - backgroundColor, + borderRadius, + backgroundColor: t.palette.contrast_25, } - }, [finalShape, size, backgroundColor]) + }, [finalShape, size, t]) + + const borderStyle = useMemo(() => { + return [ + {borderRadius: aviStyle.borderRadius}, + live && { + borderColor: t.palette.negative_500, + borderWidth: size > 16 ? 2 : 1, + opacity: 1, + }, + ] + }, [aviStyle.borderRadius, live, t, size]) const alert = useMemo(() => { if (!moderation?.alert) { @@ -277,12 +298,19 @@ let UserAvatar = ({ onLoad={onLoad} /> )} - <MediaInsetBorder style={[{borderRadius: aviStyle.borderRadius}]} /> + <MediaInsetBorder style={borderStyle} /> + {live && size > 16 && !hideLiveBadge && ( + <LiveIndicator size={size > 32 ? 'small' : 'tiny'} /> + )} {alert} </View> ) : ( <View style={containerStyle}> <DefaultAvatar type={type} shape={finalShape} size={size} /> + <MediaInsetBorder style={borderStyle} /> + {live && size > 16 && !hideLiveBadge && ( + <LiveIndicator size={size > 32 ? 'small' : 'tiny'} /> + )} {alert} </View> ) @@ -486,21 +514,32 @@ let PreviewableUserAvatar = ({ disableHoverCard, disableNavigation, onBeforePress, + live, ...rest }: PreviewableUserAvatarProps): React.ReactNode => { const {_} = useLingui() const queryClient = useQueryClient() + const status = useActorStatus(profile) + const liveControl = useDialogControl() + const playHaptic = useHaptics() - const onPress = React.useCallback(() => { + const onPress = useCallback(() => { onBeforePress?.() unstableCacheProfileView(queryClient, profile) }, [profile, queryClient, onBeforePress]) + const onOpenLiveStatus = useCallback(() => { + playHaptic('Light') + logger.metric('live:card:open', {subject: profile.did, from: 'post'}) + liveControl.open() + }, [liveControl, playHaptic, profile.did]) + const avatarEl = ( <UserAvatar avatar={profile.avatar} moderation={moderation} type={profile.associated?.labeler ? 'labeler' : 'user'} + live={status.isActive || live} {...rest} /> ) @@ -509,9 +548,32 @@ let PreviewableUserAvatar = ({ <ProfileHoverCard did={profile.did} disable={disableHoverCard}> {disableNavigation ? ( avatarEl + ) : status.isActive && (isNative || isTouchDevice) ? ( + <> + <Button + label={_( + msg`${sanitizeDisplayName( + profile.displayName || sanitizeHandle(profile.handle), + )}'s avatar`, + )} + accessibilityHint={_(msg`Opens live status dialog`)} + onPress={onOpenLiveStatus}> + {avatarEl} + </Button> + <LiveStatusDialog + control={liveControl} + profile={profile} + status={status} + embed={status.embed} + /> + </> ) : ( <Link - label={_(msg`${profile.displayName || profile.handle}'s avatar`)} + label={_( + msg`${sanitizeDisplayName( + profile.displayName || sanitizeHandle(profile.handle), + )}'s avatar`, + )} accessibilityHint={_(msg`Opens this profile`)} to={makeProfileLink({ did: profile.did, |