Update presentation of chat requests (#10417)
This commit is contained in:
@@ -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 you’re following</Trans>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user