diff options
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/AvatarStack.tsx | 52 | ||||
-rw-r--r-- | src/components/KnownFollowers.tsx | 29 | ||||
-rw-r--r-- | src/components/dms/LeaveConvoPrompt.tsx | 6 | ||||
-rw-r--r-- | src/components/dms/MessageProfileButton.tsx | 45 | ||||
-rw-r--r-- | src/components/dms/MessagesListHeader.tsx | 6 | ||||
-rw-r--r-- | src/components/dms/ReportDialog.tsx | 25 | ||||
-rw-r--r-- | src/components/icons/CircleX.tsx | 5 |
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', +}) |