about summary refs log tree commit diff
path: root/src/view/com/util/UserAvatar.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/util/UserAvatar.tsx')
-rw-r--r--src/view/com/util/UserAvatar.tsx88
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,