Update presentation of chat requests (#10417)

This commit is contained in:
DS Boyce
2026-05-08 12:05:05 -07:00
committed by GitHub
parent c8e4815837
commit a889a1b081
5 changed files with 160 additions and 102 deletions
+6 -9
View File
@@ -5,9 +5,7 @@ import {
moderateProfile,
type ModerationOpts,
} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/react/macro'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
@@ -93,7 +91,7 @@ function KnownFollowersInner({
showIfEmpty?: boolean
}) {
const t = useTheme()
const {_} = useLingui()
const {t: l} = useLingui()
const textStyle = [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]
@@ -124,9 +122,7 @@ function KnownFollowersInner({
return (
<Link
label={_(
msg`Press to view followers of this account that you also follow`,
)}
label={l`Press to view followers of this account that you also follow`}
onPress={onLinkPress}
to={makeProfileLink(profile, 'known-followers')}
style={[
@@ -188,7 +184,8 @@ function KnownFollowersInner({
numberOfLines={2}>
{slice.length >= 2 ? (
// 2-n followers, including blocks
serverCount > 2 ? ( // only 2
// only 2
serverCount > 2 ? (
<Trans>
Followed by{' '}
<Text emoji key={slice[0].profile.did} style={textStyle}>
@@ -254,7 +251,7 @@ function EmptyFallback({show}: {show?: boolean}) {
return (
<Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>Not followed by anyone you're following</Trans>
<Trans>Not followed by anyone youre following</Trans>
</Text>
)
}
+7 -10
View File
@@ -1,5 +1,4 @@
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useLingui} from '@lingui/react/macro'
import {StackActions, useNavigation} from '@react-navigation/native'
import {type NavigationProp} from '#/lib/routes/types'
@@ -20,7 +19,7 @@ export function LeaveConvoPrompt({
currentScreen: 'list' | 'conversation'
hasMessages?: boolean
}) {
const {_} = useLingui()
const {t: l} = useLingui()
const navigation = useNavigation<NavigationProp>()
const {mutate: leaveConvo} = useLeaveConvo(convoId, {
@@ -32,7 +31,7 @@ export function LeaveConvoPrompt({
}
},
onError: () => {
Toast.show(_(msg`Could not leave chat`), {
Toast.show(l`Could not leave chat`, {
type: 'error',
})
},
@@ -41,15 +40,13 @@ export function LeaveConvoPrompt({
return (
<Prompt.Basic
control={control}
title={_(msg`Leave conversation`)}
title={l`Leave conversation`}
description={
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?`)
? l`Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant.`
: l`Are you sure you want to leave this conversation?`
}
confirmButtonCta={_(msg`Leave`)}
confirmButtonCta={l`Leave`}
confirmButtonColor="negative"
onConfirm={() => leaveConvo()}
/>
@@ -1,19 +1,26 @@
import {useCallback} from 'react'
import {useCallback, useMemo} from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {LinearGradient} from 'expo-linear-gradient'
import {ChatBskyConvoDefs, moderateProfile} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {type ActiveConvoStates} from '#/state/messages/convo'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {atoms as a, useTheme} from '#/alf'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme, web} from '#/alf'
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
import {KnownFollowers} from '#/components/KnownFollowers'
import {usePromptControl} from '#/components/Prompt'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
import {AcceptChatButton, DeleteChatButton, RejectMenu} from './RequestButtons'
export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) {
const t = useTheme()
const {_} = useLingui()
const {t: l} = useLingui()
const moderationOpts = useModerationOpts()
const leaveConvoControl = usePromptControl()
@@ -25,36 +32,59 @@ export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) {
// if we ever allow someone other than the owner to invite people, this will need to change
const otherUser = convoState.convo.primaryMember
const lastMessage = ChatBskyConvoDefs.isMessageView(
convoState.convo.view.lastMessage,
)
? convoState.convo.view.lastMessage
: null
if (!moderationOpts) {
return null
}
return (
<View style={[t.atoms.bg, a.p_lg, a.gap_md, a.align_center]}>
<View style={[a.gap_md, a.p_2xl, t.atoms.bg]}>
<LinearGradient
colors={['rgba(0,0,0,0)', 'rgba(0,0,0,0.08)']}
style={[a.absolute, {top: -16, left: 0, right: 0, height: 16}]}
pointerEvents="none"
/>
{otherUser && (
<InviterHeader profile={otherUser} moderationOpts={moderationOpts} />
)}
{otherUser && (
<KnownFollowers
profile={otherUser}
moderationOpts={moderationOpts}
minimal
showIfEmpty
/>
)}
<View style={[a.flex_row, a.gap_md, a.w_full, otherUser && a.pt_sm]}>
{otherUser && (
<RejectMenu
label={_(msg`Block or report`)}
label={lastMessage ? l`Block or report` : l`Block`}
icon={true}
convo={convoState.convo.view}
profile={otherUser}
color="negative_subtle"
size="small"
size="large"
currentScreen="conversation"
style={[a.flex_1]}
/>
)}
<DeleteChatButton
label={_(msg`Delete`)}
label={l({
message: 'Leave',
comment: 'Leave a conversation (reject a chat invitation)',
context: 'Button',
})}
icon={true}
convo={convoState.convo.view}
color="secondary"
size="small"
size="large"
currentScreen="conversation"
style={[a.flex_1]}
onPress={leaveConvoControl.open}
/>
<LeaveConvoPrompt
@@ -66,13 +96,53 @@ export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) {
</View>
<View style={[a.w_full, a.flex_row]}>
<AcceptChatButton
icon={true}
onAcceptConvo={onAcceptChat}
convo={convoState.convo.view}
color="primary_subtle"
size="small"
color="primary"
size="large"
currentScreen="conversation"
style={[a.flex_1]}
/>
</View>
</View>
)
}
function InviterHeader({
profile: profileUnshadowed,
moderationOpts,
}: {
profile: bsky.profile.AnyProfileView
moderationOpts: NonNullable<ReturnType<typeof useModerationOpts>>
}) {
const t = useTheme()
const profile = useProfileShadow(profileUnshadowed)
const moderation = useMemo(
() => moderateProfile(profile, moderationOpts),
[profile, moderationOpts],
)
const displayName = createSanitizedDisplayName(
profile,
true,
moderation.ui('displayName'),
)
return (
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<PreviewableUserAvatar
profile={profile}
size={42}
moderation={moderation.ui('avatar')}
/>
<View>
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
<Trans>{displayName} added you</Trans>
</Text>
<Text style={[web(a.pt_xs), a.text_sm, t.atoms.text_contrast_high]}>
{sanitizeHandle(profile.handle, '@')}
</Text>
</View>
</View>
)
}
@@ -1,8 +1,6 @@
import {useCallback} from 'react'
import {type ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {Trans, useLingui} from '@lingui/react/macro'
import {StackActions, useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
@@ -16,7 +14,6 @@ import {
unstableCacheProfileView,
useProfileBlockMutationQueue,
} from '#/state/queries/profile'
import {atoms as a} from '#/alf'
import {
Button,
ButtonIcon,
@@ -29,6 +26,8 @@ import {
useEmailDialogControl,
} from '#/components/dialogs/EmailDialog'
import {AfterReportDialog} from '#/components/dms/AfterReportDialog'
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft'
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
import {CircleX_Stroke2_Corner0_Rounded} from '#/components/icons/CircleX'
import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag'
import {PersonX_Stroke2_Corner0_Rounded as PersonXIcon} from '#/components/icons/Person'
@@ -40,20 +39,22 @@ import * as Toast from '#/components/Toast'
export function RejectMenu({
convo,
profile,
size = 'tiny',
size = 'small',
color = 'secondary',
label,
icon = false,
showDeleteConvo,
currentScreen,
...props
}: Omit<ButtonProps, 'onPress' | 'children' | 'label'> & {
label?: string
icon?: boolean
convo: ChatBskyConvoDefs.ConvoView
profile: ChatBskyActorDefs.ProfileViewBasic
showDeleteConvo?: boolean
currentScreen: 'list' | 'conversation'
}) {
const {_} = useLingui()
const {t: l} = useLingui()
const shadowedProfile = useProfileShadow(profile)
const navigation = useNavigation<NavigationProp>()
const queryClient = useQueryClient()
@@ -66,12 +67,10 @@ export function RejectMenu({
},
onError: () => {
Toast.show(
_(
msg({
context: 'toast',
message: 'Failed to delete chat',
}),
),
l({
context: 'toast',
message: 'Failed to delete chat',
}),
{
type: 'error',
},
@@ -82,35 +81,31 @@ export function RejectMenu({
const onPressDelete = useCallback(() => {
Toast.show(
_(
msg({
context: 'toast',
message: 'Chat deleted',
}),
),
l({
context: 'toast',
message: 'Chat deleted',
}),
{
type: 'success',
},
)
leaveConvo()
}, [leaveConvo, _])
}, [leaveConvo, l])
const onPressBlock = useCallback(() => {
Toast.show(
_(
msg({
context: 'toast',
message: 'Account blocked',
}),
),
l({
context: 'toast',
message: 'Account blocked',
}),
{
type: 'success',
},
)
// block and also delete convo
queueBlock()
void queueBlock()
leaveConvo()
}, [queueBlock, leaveConvo, _])
}, [queueBlock, leaveConvo, l])
const reportControl = useDialogControl()
const blockOrDeleteControl = useDialogControl()
@@ -122,15 +117,15 @@ export function RejectMenu({
return (
<>
<Menu.Root>
<Menu.Trigger label={_(msg`Reject chat request`)}>
<Menu.Trigger label={l`Reject chat request`}>
{({props: triggerProps}) => (
<Button
{...triggerProps}
{...props}
label={triggerProps.accessibilityLabel}
style={[a.flex_1]}
color={color}
size={size}>
{icon ? <ButtonIcon icon={FlagIcon} /> : null}
<ButtonText>
{label || (
<Trans comment="Reject a chat request, this opens a menu with options">
@@ -144,27 +139,25 @@ export function RejectMenu({
<Menu.Outer showCancel>
<Menu.Group>
{showDeleteConvo && (
<Menu.Item
label={_(msg`Delete conversation`)}
onPress={onPressDelete}>
<Menu.Item label={l`Delete conversation`} onPress={onPressDelete}>
<Menu.ItemText>
<Trans>Delete conversation</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={CircleX_Stroke2_Corner0_Rounded} />
</Menu.Item>
)}
<Menu.Item label={_(msg`Block account`)} onPress={onPressBlock}>
<Menu.Item label={l`Block account`} onPress={onPressBlock}>
<Menu.ItemText>
<Trans>Block account</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={PersonXIcon} />
</Menu.Item>
{/* note: last message will almost certainly be defined, since you can't
delete messages for other people andit's impossible for a convo on this
delete messages for other people and it's impossible for a convo on this
screen to have a message sent by you */}
{lastMessage && (
<Menu.Item
label={_(msg`Report conversation`)}
label={l`Report conversation`}
onPress={reportControl.open}>
<Menu.ItemText>
<Trans>Report conversation</Trans>
@@ -210,19 +203,21 @@ export function RejectMenu({
export function AcceptChatButton({
convo,
size = 'tiny',
color = 'secondary_inverted',
size = 'small',
color = 'primary',
label,
icon = false,
currentScreen,
onAcceptConvo,
...props
}: Omit<ButtonProps, 'onPress' | 'children' | 'label'> & {
label?: string
icon?: boolean
convo: ChatBskyConvoDefs.ConvoView
onAcceptConvo?: () => void
currentScreen: 'list' | 'conversation'
}) {
const {_} = useLingui()
const {t: l} = useLingui()
const queryClient = useQueryClient()
const navigation = useNavigation<NavigationProp>()
const {needsEmailVerification} = useEmail()
@@ -245,12 +240,10 @@ export function AcceptChatButton({
// automatically. The only difference is that when they back out of the convo (without sending a message), the conversation will be rejected.
// the list will still have this chat in it -sfn
Toast.show(
_(
msg({
context: 'toast',
message: 'Failed to accept chat',
}),
),
l({
context: 'toast',
message: 'Failed to accept chat',
}),
{
type: 'error',
},
@@ -274,38 +267,43 @@ export function AcceptChatButton({
}
}, [acceptConvo, needsEmailVerification, emailDialogControl])
let Icon: React.ReactNode = null
if (isPending) {
Icon = <ButtonIcon icon={Loader} />
} else if (icon) {
Icon = <ButtonIcon icon={CheckIcon} />
}
return (
<Button
{...props}
label={label || _(msg`Accept chat request`)}
label={label || l`Accept chat request`}
size={size}
color={color}
style={a.flex_1}
onPress={onPressAccept}>
{isPending ? (
<ButtonIcon icon={Loader} />
) : (
<ButtonText>
{label || <Trans comment="Accept a chat request">Accept</Trans>}
</ButtonText>
)}
{Icon}
<ButtonText>
{label || <Trans comment="Accept a chat request">Accept</Trans>}
</ButtonText>
</Button>
)
}
export function DeleteChatButton({
convo,
size = 'tiny',
size = 'small',
color = 'secondary',
label,
icon = false,
currentScreen,
...props
}: Omit<ButtonProps, 'children' | 'label'> & {
label?: string
icon?: boolean
convo: ChatBskyConvoDefs.ConvoView
currentScreen: 'list' | 'conversation'
}) {
const {_} = useLingui()
const {t: l} = useLingui()
const navigation = useNavigation<NavigationProp>()
const {mutate: leaveConvo} = useLeaveConvo(convo.id, {
@@ -316,12 +314,10 @@ export function DeleteChatButton({
},
onError: () => {
Toast.show(
_(
msg({
context: 'toast',
message: 'Failed to delete chat',
}),
),
l({
context: 'toast',
message: 'Failed to delete chat',
}),
{
type: 'error',
},
@@ -331,27 +327,25 @@ export function DeleteChatButton({
const onPressDelete = useCallback(() => {
Toast.show(
_(
msg({
context: 'toast',
message: 'Chat deleted',
}),
),
l({
context: 'toast',
message: 'Chat deleted',
}),
{
type: 'success',
},
)
leaveConvo()
}, [leaveConvo, _])
}, [leaveConvo, l])
return (
<Button
label={label || _(msg`Delete chat`)}
label={label || l`Delete chat`}
size={size}
color={color}
style={a.flex_1}
onPress={onPressDelete}
{...props}>
{icon ? <ButtonIcon icon={LeaveIcon} /> : null}
<ButtonText>{label || <Trans>Delete chat</Trans>}</ButtonText>
</Button>
)
@@ -42,7 +42,7 @@ export function RequestListItem({
</View>
)}
{/* spacer, since you can't nest pressables */}
<View style={[a.pt_md, a.pb_xs, a.w_full, {opacity: 0}]} aria-hidden>
<View style={[a.py_lg, a.w_full, {opacity: 0}]} aria-hidden>
{/* Placeholder text so that it responds to the font height */}
<Text style={[a.text_xs, a.leading_tight, a.font_semi_bold]}>
<Trans comment="Accept a chat request">Accept Request</Trans>