about summary refs log tree commit diff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/AvatarStack.tsx52
-rw-r--r--src/components/KnownFollowers.tsx29
-rw-r--r--src/components/dms/LeaveConvoPrompt.tsx6
-rw-r--r--src/components/dms/MessageProfileButton.tsx45
-rw-r--r--src/components/dms/MessagesListHeader.tsx6
-rw-r--r--src/components/dms/ReportDialog.tsx25
-rw-r--r--src/components/icons/CircleX.tsx5
7 files changed, 123 insertions, 45 deletions
diff --git a/src/components/AvatarStack.tsx b/src/components/AvatarStack.tsx
index 1b27a95ac..63f5ed77a 100644
--- a/src/components/AvatarStack.tsx
+++ b/src/components/AvatarStack.tsx
@@ -1,37 +1,37 @@
 import {View} from 'react-native'
 import {moderateProfile} from '@atproto/api'
 
+import {logger} from '#/logger'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useProfilesQuery} from '#/state/queries/profile'
 import {UserAvatar} from '#/view/com/util/UserAvatar'
 import {atoms as a, useTheme} from '#/alf'
+import * as bsky from '#/types/bsky'
 
 export function AvatarStack({
   profiles,
   size = 26,
+  numPending,
+  backgroundColor,
 }: {
-  profiles: string[]
+  profiles: bsky.profile.AnyProfileView[]
   size?: number
+  numPending?: number
+  backgroundColor?: string
 }) {
   const halfSize = size / 2
-  const {data, error} = useProfilesQuery({handles: profiles})
   const t = useTheme()
   const moderationOpts = useModerationOpts()
 
-  if (error) {
-    console.error(error)
-    return null
-  }
-
-  const isPending = !data || !moderationOpts
+  const isPending = (numPending && profiles.length === 0) || !moderationOpts
 
   const items = isPending
-    ? Array.from({length: profiles.length}).map((_, i) => ({
+    ? Array.from({length: numPending ?? profiles.length}).map((_, i) => ({
         key: i,
         profile: null,
         moderation: null,
       }))
-    : data.profiles.map(item => ({
+    : profiles.map(item => ({
         key: item.did,
         profile: item,
         moderation: moderateProfile(item, moderationOpts),
@@ -56,7 +56,7 @@ export function AvatarStack({
               height: size,
               left: i * -halfSize,
               borderWidth: 1,
-              borderColor: t.atoms.bg.backgroundColor,
+              borderColor: backgroundColor ?? t.atoms.bg.backgroundColor,
               borderRadius: 999,
               zIndex: 3 - i,
             },
@@ -74,3 +74,33 @@ export function AvatarStack({
     </View>
   )
 }
+
+export function AvatarStackWithFetch({
+  profiles,
+  size,
+  backgroundColor,
+}: {
+  profiles: string[]
+  size?: number
+  backgroundColor?: string
+}) {
+  const {data, error} = useProfilesQuery({handles: profiles})
+
+  if (error) {
+    if (error.name !== 'AbortError') {
+      logger.error('Error fetching profiles for AvatarStack', {
+        safeMessage: error,
+      })
+    }
+    return null
+  }
+
+  return (
+    <AvatarStack
+      numPending={profiles.length}
+      profiles={data?.profiles || []}
+      size={size}
+      backgroundColor={backgroundColor}
+    />
+  )
+}
diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx
index 1e7cf448a..a883066ca 100644
--- a/src/components/KnownFollowers.tsx
+++ b/src/components/KnownFollowers.tsx
@@ -33,11 +33,13 @@ export function KnownFollowers({
   moderationOpts,
   onLinkPress,
   minimal,
+  showIfEmpty,
 }: {
   profile: bsky.profile.AnyProfileView
   moderationOpts: ModerationOpts
   onLinkPress?: LinkProps['onPress']
   minimal?: boolean
+  showIfEmpty?: boolean
 }) {
   const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
     new Map(),
@@ -64,11 +66,12 @@ export function KnownFollowers({
         moderationOpts={moderationOpts}
         onLinkPress={onLinkPress}
         minimal={minimal}
+        showIfEmpty={showIfEmpty}
       />
     )
   }
 
-  return null
+  return <EmptyFallback show={showIfEmpty} />
 }
 
 function KnownFollowersInner({
@@ -77,22 +80,19 @@ function KnownFollowersInner({
   cachedKnownFollowers,
   onLinkPress,
   minimal,
+  showIfEmpty,
 }: {
   profile: bsky.profile.AnyProfileView
   moderationOpts: ModerationOpts
   cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
   onLinkPress?: LinkProps['onPress']
   minimal?: boolean
+  showIfEmpty?: boolean
 }) {
   const t = useTheme()
   const {_} = useLingui()
 
-  const textStyle = [
-    a.flex_1,
-    a.text_sm,
-    a.leading_snug,
-    t.atoms.text_contrast_medium,
-  ]
+  const textStyle = [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]
 
   const slice = cachedKnownFollowers.followers.slice(0, 3).map(f => {
     const moderation = moderateProfile(f, moderationOpts)
@@ -115,7 +115,7 @@ function KnownFollowersInner({
    * We check above too, but here for clarity and a reminder to _check for
    * valid indices_
    */
-  if (slice.length === 0) return null
+  if (slice.length === 0) return <EmptyFallback show={showIfEmpty} />
 
   const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE
 
@@ -127,7 +127,6 @@ function KnownFollowersInner({
       onPress={onLinkPress}
       to={makeProfileLink(profile, 'known-followers')}
       style={[
-        a.flex_1,
         a.flex_row,
         minimal ? a.gap_sm : a.gap_md,
         a.align_center,
@@ -243,3 +242,15 @@ function KnownFollowersInner({
     </Link>
   )
 }
+
+function EmptyFallback({show}: {show?: boolean}) {
+  const t = useTheme()
+
+  if (!show) return null
+
+  return (
+    <Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
+      <Trans>Not followed by anyone you're following</Trans>
+    </Text>
+  )
+}
diff --git a/src/components/dms/LeaveConvoPrompt.tsx b/src/components/dms/LeaveConvoPrompt.tsx
index c99f8d063..57acd5ca7 100644
--- a/src/components/dms/LeaveConvoPrompt.tsx
+++ b/src/components/dms/LeaveConvoPrompt.tsx
@@ -13,10 +13,12 @@ export function LeaveConvoPrompt({
   control,
   convoId,
   currentScreen,
+  hasMessages = true,
 }: {
   control: DialogOuterProps['control']
   convoId: string
   currentScreen: 'list' | 'conversation'
+  hasMessages?: boolean
 }) {
   const {_} = useLingui()
   const navigation = useNavigation<NavigationProp>()
@@ -39,7 +41,9 @@ export function LeaveConvoPrompt({
       control={control}
       title={_(msg`Leave conversation`)}
       description={_(
-        msg`Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant.`,
+        hasMessages
+          ? msg`Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant.`
+          : msg`Are you sure you want to leave this conversation?`,
       )}
       confirmButtonCta={_(msg`Leave`)}
       confirmButtonColor="negative"
diff --git a/src/components/dms/MessageProfileButton.tsx b/src/components/dms/MessageProfileButton.tsx
index 5eac7f5c5..7f31f550c 100644
--- a/src/components/dms/MessageProfileButton.tsx
+++ b/src/components/dms/MessageProfileButton.tsx
@@ -8,13 +8,15 @@ import {useNavigation} from '@react-navigation/native'
 import {useEmail} from '#/lib/hooks/useEmail'
 import {NavigationProp} from '#/lib/routes/types'
 import {logEvent} from '#/lib/statsig/statsig'
-import {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members'
+import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability'
+import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
+import * as Toast from '#/view/com/util/Toast'
 import {atoms as a, useTheme} from '#/alf'
 import {Button, ButtonIcon} from '#/components/Button'
+import {useDialogControl} from '#/components/Dialog'
+import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
 import {canBeMessaged} from '#/components/dms/util'
 import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message'
-import {useDialogControl} from '../Dialog'
-import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog'
 
 export function MessageProfileButton({
   profile,
@@ -27,10 +29,19 @@ export function MessageProfileButton({
   const {needsEmailVerification} = useEmail()
   const verifyEmailControl = useDialogControl()
 
-  const {data: convo, isPending} = useMaybeConvoForUser(profile.did)
+  const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did)
+  const {mutate: initiateConvo} = useGetConvoForMembers({
+    onSuccess: ({convo}) => {
+      logEvent('chat:open', {logContext: 'ProfileHeader'})
+      navigation.navigate('MessagesConversation', {conversation: convo.id})
+    },
+    onError: () => {
+      Toast.show(_(msg`Failed to create conversation`))
+    },
+  })
 
   const onPress = React.useCallback(() => {
-    if (!convo?.id) {
+    if (!convoAvailability?.canChat) {
       return
     }
 
@@ -39,15 +50,25 @@ export function MessageProfileButton({
       return
     }
 
-    if (convo && !convo.lastMessage) {
+    if (convoAvailability.convo) {
+      logEvent('chat:open', {logContext: 'ProfileHeader'})
+      navigation.navigate('MessagesConversation', {
+        conversation: convoAvailability.convo.id,
+      })
+    } else {
       logEvent('chat:create', {logContext: 'ProfileHeader'})
+      initiateConvo([profile.did])
     }
-    logEvent('chat:open', {logContext: 'ProfileHeader'})
-
-    navigation.navigate('MessagesConversation', {conversation: convo.id})
-  }, [needsEmailVerification, verifyEmailControl, convo, navigation])
+  }, [
+    needsEmailVerification,
+    verifyEmailControl,
+    navigation,
+    profile.did,
+    initiateConvo,
+    convoAvailability,
+  ])
 
-  if (isPending) {
+  if (!convoAvailability) {
     // show pending state based on declaration
     if (canBeMessaged(profile)) {
       return (
@@ -69,7 +90,7 @@ export function MessageProfileButton({
     }
   }
 
-  if (convo) {
+  if (convoAvailability.canChat) {
     return (
       <>
         <Button
diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx
index 7c35c30ba..8da8c015f 100644
--- a/src/components/dms/MessagesListHeader.tsx
+++ b/src/components/dms/MessagesListHeader.tsx
@@ -53,10 +53,10 @@ export let MessagesListHeader = ({
   }, [moderation])
 
   const onPressBack = useCallback(() => {
-    if (isWeb) {
-      navigation.replace('Messages', {})
-    } else {
+    if (navigation.canGoBack()) {
       navigation.goBack()
+    } else {
+      navigation.navigate('Messages', {})
     }
   }, [navigation])
 
diff --git a/src/components/dms/ReportDialog.tsx b/src/components/dms/ReportDialog.tsx
index 71cca897a..c1ea854f9 100644
--- a/src/components/dms/ReportDialog.tsx
+++ b/src/components/dms/ReportDialog.tsx
@@ -311,6 +311,19 @@ function DoneStep({
     },
   })
 
+  let btnText = _(msg`Done`)
+  let toastMsg: string | undefined
+  if (actions.includes('leave') && actions.includes('block')) {
+    btnText = _(msg`Block and Delete`)
+    toastMsg = _(msg`Conversation deleted`)
+  } else if (actions.includes('leave')) {
+    btnText = _(msg`Delete Conversation`)
+    toastMsg = _(msg`Conversation deleted`)
+  } else if (actions.includes('block')) {
+    btnText = _(msg`Block User`)
+    toastMsg = _(msg`User blocked`)
+  }
+
   const onPressPrimaryAction = () => {
     control.close(() => {
       if (actions.includes('block')) {
@@ -319,18 +332,12 @@ function DoneStep({
       if (actions.includes('leave')) {
         leaveConvo()
       }
+      if (toastMsg) {
+        Toast.show(toastMsg, 'check')
+      }
     })
   }
 
-  let btnText = _(msg`Done`)
-  if (actions.includes('leave') && actions.includes('block')) {
-    btnText = _(msg`Block and Delete`)
-  } else if (actions.includes('leave')) {
-    btnText = _(msg`Delete Conversation`)
-  } else if (actions.includes('block')) {
-    btnText = _(msg`Block User`)
-  }
-
   return (
     <View style={a.gap_2xl}>
       <View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
diff --git a/src/components/icons/CircleX.tsx b/src/components/icons/CircleX.tsx
new file mode 100644
index 000000000..e840bd09e
--- /dev/null
+++ b/src/components/icons/CircleX.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const CircleX_Stroke2_Corner0_Rounded = createSinglePathSVG({
+  path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm6.293-3.707a1 1 0 0 1 1.414 0L12 10.586l2.293-2.293a1 1 0 1 1 1.414 1.414L13.414 12l2.293 2.293a1 1 0 0 1-1.414 1.414L12 13.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L10.586 12 8.293 9.707a1 1 0 0 1 0-1.414Z',
+})