Files
bsky-social-app/src/components/dms/MessagesListHeader.tsx
T
2026-05-25 04:12:40 -07:00

256 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {useMemo} from 'react'
import {View} from 'react-native'
import {
ChatBskyConvoDefs,
moderateProfile,
type ModerationOpts,
} from '@atproto/api'
import {useLingui} from '@lingui/react/macro'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {makeProfileLink} from '#/lib/routes/links'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSession} from '#/state/session'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {useIsWithinSplitView} from '#/screens/Messages/components/splitView/context'
import {atoms as a, useTheme} from '#/alf'
import {AvatarBubbles} from '#/components/AvatarBubbles'
import {ButtonIcon} from '#/components/Button'
import {ConvoMenu} from '#/components/dms/ConvoMenu'
import {Bell2Off_Filled_Corner0_Rounded as BellOffIcon} from '#/components/icons/Bell2'
import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
import * as Layout from '#/components/Layout'
import {Link} from '#/components/Link'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography'
import {IS_LIQUID_GLASS, IS_WEB} from '#/env'
import {type ConvoWithDetails} from './util'
const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE
export function MessagesListHeader({convo}: {convo?: ConvoWithDetails | null}) {
const t = useTheme()
const moderationOpts = useModerationOpts()
const {isWithinSplitView} = useIsWithinSplitView()
return (
<Layout.Header.Outer noBottomBorder={IS_LIQUID_GLASS}>
<View style={[a.w_full, a.flex_row, a.gap_xs, a.align_start]}>
{!isWithinSplitView && (
<View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
<Layout.Header.BackButton />
</View>
)}
{convo && moderationOpts ? (
convo.kind === 'direct' ? (
<ProfileHeaderReady convo={convo} moderationOpts={moderationOpts} />
) : (
<GroupHeaderReady convo={convo} />
)
) : (
<>
<View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1]}>
<View
style={[
{width: PFP_SIZE, height: PFP_SIZE},
a.rounded_full,
t.atoms.bg_contrast_25,
]}
/>
<View style={a.gap_xs}>
<View
style={[
{width: 150, height: 16},
a.rounded_xs,
t.atoms.bg_contrast_25,
a.mt_xs,
]}
/>
</View>
</View>
<Layout.Header.Slot />
</>
)}
</View>
</Layout.Header.Outer>
)
}
function ProfileHeaderReady({
convo,
moderationOpts,
}: {
convo: Extract<ConvoWithDetails, {kind: 'direct'}>
moderationOpts: ModerationOpts
}) {
const {t: l} = useLingui()
const {currentAccount} = useSession()
const profile = useProfileShadow(convo.primaryMember)
const moderation = moderateProfile(profile, moderationOpts)
const blockInfo = useMemo(() => {
const modui = moderation.ui('profileView')
const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
const listBlocks = blocks.filter(alert => alert.source.type === 'list')
const userBlock = blocks.find(alert => alert.source.type === 'user')
return {
listBlocks,
userBlock,
}
}, [moderation])
const isDeletedAccount = profile?.handle === 'missing.invalid'
const displayName = isDeletedAccount
? l`Deleted Account`
: createSanitizedDisplayName(profile, true, moderation.ui('displayName'))
const latestReportableMessage =
ChatBskyConvoDefs.isMessageView(convo.view.lastMessage) &&
convo.view.lastMessage.sender?.did !== currentAccount?.did
? convo.view.lastMessage
: undefined
return (
<Wrapper
heading={
<Link
label={l`View ${displayName}s profile`}
style={[a.flex_row, a.gap_md, a.flex_1]}
to={makeProfileLink(profile)}>
<PreviewableUserAvatar
size={PFP_SIZE}
profile={profile}
moderation={moderation.ui('avatar')}
disableHoverCard={moderation.blocked}
/>
<View style={[a.flex_row, a.align_center, a.flex_1]}>
<Text
style={[a.text_md, a.font_semi_bold, a.flex_shrink]}
numberOfLines={1}>
{displayName}
</Text>
<ProfileBadges profile={profile} size="md" style={[a.pl_xs]} />
<MuteStatus muted={convo.view.muted} />
</View>
</Link>
}
settings={
<ConvoMenu
convo={convo.view}
profile={profile}
currentScreen="conversation"
blockInfo={blockInfo}
latestReportableMessage={latestReportableMessage}
/>
}
/>
)
}
function GroupHeaderReady({
convo,
}: {
convo: Extract<ConvoWithDetails, {kind: 'group'}>
}) {
const {t: l} = useLingui()
const disabled = convo.details.lockStatus === 'locked-permanently'
return (
<Wrapper
heading={
<Link
label={convo.details.name}
accessibilityHint={l`Open group chat settings`}
style={[a.flex_row, a.gap_md, a.flex_1, a.justify_start]}
to={
disabled
? '#'
: {
screen: 'MessagesConversationSettings',
params: {
conversation: convo.view.id,
},
}
}>
<AvatarBubbles size={40} profiles={convo.members} />
<View style={[a.flex_row, a.flex_1, a.align_center]}>
<Text
style={[a.text_md, a.font_semi_bold, a.flex_shrink]}
numberOfLines={1}>
{convo.details.name}
</Text>
<MuteStatus muted={convo.view.muted} />
</View>
</Link>
}
settings={
<Link
to={
disabled
? '#'
: {
screen: 'MessagesConversationSettings',
params: {
conversation: convo.view.id,
},
}
}
label={l`Open group chat settings`}
size="small"
color="secondary"
shape="round"
variant="ghost"
style={[a.bg_transparent, a.justify_center]}>
<ButtonIcon icon={DotsHorizontalIcon} size="md" />
</Link>
}
/>
)
}
function Wrapper({
heading,
settings,
}: {
heading: React.ReactNode
settings: React.ReactNode
}) {
return (
<View style={[a.flex_1]}>
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.justify_between,
a.gap_sm,
]}>
<View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1]}>
{heading}
</View>
<View
style={[{minHeight: PFP_SIZE}, a.justify_center, a.flex_shrink_0]}>
<Layout.Header.Slot>{settings}</Layout.Header.Slot>
</View>
</View>
</View>
)
}
function MuteStatus({muted}: {muted: boolean}) {
const t = useTheme()
return muted ? (
<>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}> &middot; </Text>
<BellOffIcon size="sm" style={t.atoms.text_contrast_medium} />
</>
) : undefined
}