From a0bd8042621e108f47e09dd096cf0d73fe1cee53 Mon Sep 17 00:00:00 2001
From: Samuel Newman <mozzius@protonmail.com>
Date: Sat, 10 May 2025 00:06:06 +0300
Subject: Live (#8354)

---
 src/screens/Profile/Header/Shell.tsx | 100 +++++++++++++++++++++++++++--------
 1 file changed, 78 insertions(+), 22 deletions(-)

(limited to 'src/screens/Profile/Header/Shell.tsx')

diff --git a/src/screens/Profile/Header/Shell.tsx b/src/screens/Profile/Header/Shell.tsx
index a3efdedf5..9e868c474 100644
--- a/src/screens/Profile/Header/Shell.tsx
+++ b/src/screens/Profile/Header/Shell.tsx
@@ -1,24 +1,35 @@
-import React, {memo} from 'react'
+import React, {memo, useEffect} from 'react'
 import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
-import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
+import {
+  type MeasuredDimensions,
+  runOnJS,
+  runOnUI,
+} from 'react-native-reanimated'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
+import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
 
+import {useActorStatus} from '#/lib/actor-status'
 import {BACK_HITSLOP} from '#/lib/constants'
+import {useHaptics} from '#/lib/haptics'
 import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
-import {NavigationProp} from '#/lib/routes/types'
+import {type NavigationProp} from '#/lib/routes/types'
+import {logger} from '#/logger'
 import {isIOS} from '#/platform/detection'
-import {Shadow} from '#/state/cache/types'
+import {type Shadow} from '#/state/cache/types'
 import {useLightboxControls} from '#/state/lightbox'
 import {useSession} from '#/state/session'
 import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 import {UserAvatar} from '#/view/com/util/UserAvatar'
 import {UserBanner} from '#/view/com/util/UserBanner'
 import {atoms as a, platform, useTheme} from '#/alf'
+import {useDialogControl} from '#/components/Dialog'
 import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
+import {EditLiveDialog} from '#/components/live/EditLiveDialog'
+import {LiveIndicator} from '#/components/live/LiveIndicator'
+import {LiveStatusDialog} from '#/components/live/LiveStatusDialog'
 import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
 import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
 import {GrowableAvatar} from './GrowableAvatar'
@@ -45,6 +56,8 @@ let ProfileHeaderShell = ({
   const {openLightbox} = useLightboxControls()
   const navigation = useNavigation<NavigationProp>()
   const {top: topInset} = useSafeAreaInsets()
+  const playHaptic = useHaptics()
+  const liveStatusControl = useDialogControl()
 
   const aviRef = useHandleRef()
 
@@ -79,24 +92,46 @@ let ProfileHeaderShell = ({
     [openLightbox],
   )
 
-  const onPressAvi = React.useCallback(() => {
-    const modui = moderation.ui('avatar')
-    const avatar = profile.avatar
-    if (avatar && !(modui.blur && modui.noOverride)) {
-      const aviHandle = aviRef.current
-      runOnUI(() => {
-        'worklet'
-        const rect = measureHandle(aviHandle)
-        runOnJS(_openLightbox)(avatar, rect)
-      })()
-    }
-  }, [profile, moderation, _openLightbox, aviRef])
-
   const isMe = React.useMemo(
     () => currentAccount?.did === profile.did,
     [currentAccount, profile],
   )
 
+  const live = useActorStatus(profile)
+
+  useEffect(() => {
+    if (live.isActive) {
+      logger.metric('live:view:profile', {subject: profile.did})
+    }
+  }, [live.isActive, profile.did])
+
+  const onPressAvi = React.useCallback(() => {
+    if (live.isActive) {
+      playHaptic('Light')
+      logger.metric('live:card:open', {subject: profile.did, from: 'profile'})
+      liveStatusControl.open()
+    } else {
+      const modui = moderation.ui('avatar')
+      const avatar = profile.avatar
+      if (avatar && !(modui.blur && modui.noOverride)) {
+        const aviHandle = aviRef.current
+        runOnUI(() => {
+          'worklet'
+          const rect = measureHandle(aviHandle)
+          runOnJS(_openLightbox)(avatar, rect)
+        })()
+      }
+    }
+  }, [
+    profile,
+    moderation,
+    _openLightbox,
+    aviRef,
+    liveStatusControl,
+    live,
+    playHaptic,
+  ])
+
   return (
     <View style={t.atoms.bg} pointerEvents={isIOS ? 'auto' : 'box-none'}>
       <View
@@ -170,21 +205,44 @@ let ProfileHeaderShell = ({
           <View
             style={[
               t.atoms.bg,
-              {borderColor: t.atoms.bg.backgroundColor},
+              a.rounded_full,
+              {
+                borderWidth: live.isActive ? 3 : 2,
+                borderColor: live.isActive
+                  ? t.palette.negative_500
+                  : t.atoms.bg.backgroundColor,
+              },
               styles.avi,
               profile.associated?.labeler && styles.aviLabeler,
             ]}>
             <View ref={aviRef} collapsable={false}>
               <UserAvatar
                 type={profile.associated?.labeler ? 'labeler' : 'user'}
-                size={90}
+                size={live.isActive ? 88 : 90}
                 avatar={profile.avatar}
                 moderation={moderation.ui('avatar')}
               />
+              {live.isActive && <LiveIndicator size="large" />}
             </View>
           </View>
         </TouchableWithoutFeedback>
       </GrowableAvatar>
+
+      {live.isActive &&
+        (isMe ? (
+          <EditLiveDialog
+            control={liveStatusControl}
+            status={live}
+            embed={live.embed}
+          />
+        ) : (
+          <LiveStatusDialog
+            control={liveStatusControl}
+            status={live}
+            embed={live.embed}
+            profile={profile}
+          />
+        ))}
     </View>
   )
 }
@@ -219,8 +277,6 @@ const styles = StyleSheet.create({
   avi: {
     width: 94,
     height: 94,
-    borderRadius: 47,
-    borderWidth: 2,
   },
   aviLabeler: {
     borderRadius: 10,
-- 
cgit 1.4.1

