update chat header

This commit is contained in:
Samuel Newman
2026-04-10 22:31:43 +03:00
parent 7ba739190d
commit e675fa4b51
4 changed files with 154 additions and 70 deletions
+36 -22
View File
@@ -5,18 +5,19 @@ import {
type ModerationCause, type ModerationCause,
type ModerationDecision, type ModerationDecision,
} from '@atproto/api' } from '@atproto/api'
import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro'
import {useLingui} from '@lingui/react'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {type Shadow} from '#/state/cache/profile-shadow' import {type Shadow} from '#/state/cache/profile-shadow'
import {isConvoActive, useConvo} from '#/state/messages/convo' import {isConvoActive, useConvo} from '#/state/messages/convo'
import {type ConvoItem} from '#/state/messages/convo/types' import {type ConvoItem} from '#/state/messages/convo/types'
import {useSession} from '#/state/session'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme, web} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {AvatarBubbles} from '#/components/AvatarBubbles'
import {ConvoMenu} from '#/components/dms/ConvoMenu' import {ConvoMenu} from '#/components/dms/ConvoMenu'
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2' import {Bell2Off_Filled_Corner0_Rounded as BellOffIcon} from '#/components/icons/Bell2'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {Link} from '#/components/Link' import {Link} from '#/components/Link'
import {PostAlerts} from '#/components/moderation/PostAlerts' import {PostAlerts} from '#/components/moderation/PostAlerts'
@@ -34,6 +35,7 @@ export function MessagesListHeader({
moderation?: ModerationDecision moderation?: ModerationDecision
}) { }) {
const t = useTheme() const t = useTheme()
const convoState = useConvo()
const blockInfo = useMemo(() => { const blockInfo = useMemo(() => {
if (!moderation) return if (!moderation) return
@@ -78,6 +80,7 @@ export function MessagesListHeader({
a.mt_xs, a.mt_xs,
]} ]}
/> />
{!convoState.isGroup?.() && (
<View <View
style={[ style={[
{width: 175, height: 12}, {width: 175, height: 12},
@@ -85,6 +88,7 @@ export function MessagesListHeader({
t.atoms.bg_contrast_25, t.atoms.bg_contrast_25,
]} ]}
/> />
)}
</View> </View>
</View> </View>
@@ -108,22 +112,28 @@ function HeaderReady({
userBlock?: ModerationCause userBlock?: ModerationCause
} }
}) { }) {
const {_} = useLingui() const {t: l} = useLingui()
const t = useTheme() const t = useTheme()
const convoState = useConvo() const convoState = useConvo()
const {currentAccount} = useSession()
const groupInfo = convoState.getGroupInfo?.()
const isGroupChat = groupInfo != null
const isDeletedAccount = profile?.handle === 'missing.invalid' const isDeletedAccount = profile?.handle === 'missing.invalid'
const displayName = isDeletedAccount const displayName = isGroupChat
? _(msg`Deleted Account`) ? (groupInfo.name ?? l`${profile.handle}'s group chat`)
: isDeletedAccount
? l`Deleted Account`
: sanitizeDisplayName( : sanitizeDisplayName(
profile.displayName || profile.handle, profile.displayName || profile.handle,
moderation.ui('displayName'), moderation.ui('displayName'),
) )
// @ts-ignore findLast is polyfilled - esb
const latestMessageFromOther = convoState.items.findLast( const latestMessageFromOther = convoState.items.findLast(
(item: ConvoItem) => (item: ConvoItem) =>
item.type === 'message' && item.message.sender.did === profile.did, item.type === 'message' &&
item.message.sender.did !== currentAccount?.did,
) )
const latestReportableMessage = const latestReportableMessage =
@@ -134,8 +144,20 @@ function HeaderReady({
return ( return (
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
<View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}> <View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}>
{isGroupChat ? (
<View
style={[a.flex_row, a.align_center, a.gap_md, a.flex_1, a.pr_md]}>
<AvatarBubbles
size="small"
profiles={convoState.recipients ?? []}
/>
<Text style={[a.text_md, a.font_semi_bold]} numberOfLines={1}>
{displayName}
</Text>
</View>
) : (
<Link <Link
label={_(msg`View ${displayName}'s profile`)} label={l`View ${displayName}'s profile`}
style={[a.flex_row, a.align_start, a.gap_md, a.flex_1, a.pr_md]} style={[a.flex_row, a.align_start, a.gap_md, a.flex_1, a.pr_md]}
to={makeProfileLink(profile)}> to={makeProfileLink(profile)}>
<PreviewableUserAvatar <PreviewableUserAvatar
@@ -148,12 +170,7 @@ function HeaderReady({
<View style={[a.flex_row, a.align_center]}> <View style={[a.flex_row, a.align_center]}>
<Text <Text
emoji emoji
style={[ style={[a.text_md, a.font_semi_bold, a.self_start]}
a.text_md,
a.font_semi_bold,
a.self_start,
web(a.leading_normal),
]}
numberOfLines={1}> numberOfLines={1}>
{displayName} {displayName}
</Text> </Text>
@@ -161,18 +178,14 @@ function HeaderReady({
</View> </View>
{!isDeletedAccount && ( {!isDeletedAccount && (
<Text <Text
style={[ style={[t.atoms.text_contrast_medium, a.text_xs]}
t.atoms.text_contrast_medium,
a.text_xs,
web([a.leading_normal, {marginTop: -2}]),
]}
numberOfLines={1}> numberOfLines={1}>
@{profile.handle} @{profile.handle}
{convoState.convo?.muted && ( {convoState.convo?.muted && (
<> <>
{' '} {' '}
&middot;{' '} &middot;{' '}
<BellStroke <BellOffIcon
size="xs" size="xs"
style={t.atoms.text_contrast_medium} style={t.atoms.text_contrast_medium}
/> />
@@ -182,6 +195,7 @@ function HeaderReady({
)} )}
</View> </View>
</Link> </Link>
)}
<View style={[{minHeight: PFP_SIZE}, a.justify_center]}> <View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
<Layout.Header.Slot> <Layout.Header.Slot>
+3 -3
View File
@@ -101,7 +101,7 @@ function Inner() {
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const {data: recipientUnshadowed} = useProfileQuery({ const {data: recipientUnshadowed} = useProfileQuery({
did: convoState.recipients?.[0].did, did: convoState.getPrimaryMember?.()?.did,
}) })
const recipient = useMaybeProfileShadow(recipientUnshadowed) const recipient = useMaybeProfileShadow(recipientUnshadowed)
@@ -135,7 +135,7 @@ function Inner() {
<> <>
<Layout.Center style={[a.flex_1]}> <Layout.Center style={[a.flex_1]}>
{moderation ? ( {moderation ? (
<MessagesListHeader moderation={moderation} profile={recipient} /> <MessagesListHeader profile={recipient} moderation={moderation} />
) : ( ) : (
<MessagesListHeader /> <MessagesListHeader />
)} )}
@@ -156,7 +156,7 @@ function Inner() {
{isFocused && IS_WEB && <RemoveScrollBar />} {isFocused && IS_WEB && <RemoveScrollBar />}
{!readyToShow && {!readyToShow &&
(moderation ? ( (moderation ? (
<MessagesListHeader moderation={moderation} profile={recipient} /> <MessagesListHeader profile={recipient} moderation={moderation} />
) : ( ) : (
<MessagesListHeader /> <MessagesListHeader />
))} ))}
+46
View File
@@ -37,6 +37,7 @@ import {
import {type MessagesEventBus} from '#/state/messages/events/agent' import {type MessagesEventBus} from '#/state/messages/events/agent'
import {type MessagesEventBusError} from '#/state/messages/events/types' import {type MessagesEventBusError} from '#/state/messages/events/types'
import {IS_NATIVE} from '#/env' import {IS_NATIVE} from '#/env'
import * as bsky from '#/types/bsky'
const logger = Logger.create(Logger.Context.ConversationAgent) const logger = Logger.create(Logger.Context.ConversationAgent)
@@ -112,6 +113,9 @@ export class Convo {
this.markConvoAccepted = this.markConvoAccepted.bind(this) this.markConvoAccepted = this.markConvoAccepted.bind(this)
this.addReaction = this.addReaction.bind(this) this.addReaction = this.addReaction.bind(this)
this.removeReaction = this.removeReaction.bind(this) this.removeReaction = this.removeReaction.bind(this)
this.isGroup = this.isGroup.bind(this)
this.getGroupInfo = this.getGroupInfo.bind(this)
this.getPrimaryMember = this.getPrimaryMember.bind(this)
} }
private commit() { private commit() {
@@ -155,6 +159,9 @@ export class Convo {
markConvoAccepted: undefined, markConvoAccepted: undefined,
addReaction: undefined, addReaction: undefined,
removeReaction: undefined, removeReaction: undefined,
isGroup: this.isGroup,
getGroupInfo: this.getGroupInfo,
getPrimaryMember: this.getPrimaryMember,
} }
} }
case ConvoStatus.Disabled: case ConvoStatus.Disabled:
@@ -175,6 +182,9 @@ export class Convo {
markConvoAccepted: this.markConvoAccepted, markConvoAccepted: this.markConvoAccepted,
addReaction: this.addReaction, addReaction: this.addReaction,
removeReaction: this.removeReaction, removeReaction: this.removeReaction,
isGroup: this.isGroup,
getGroupInfo: this.getGroupInfo,
getPrimaryMember: this.getPrimaryMember,
} }
} }
case ConvoStatus.Error: { case ConvoStatus.Error: {
@@ -192,6 +202,9 @@ export class Convo {
markConvoAccepted: undefined, markConvoAccepted: undefined,
addReaction: undefined, addReaction: undefined,
removeReaction: undefined, removeReaction: undefined,
isGroup: undefined,
getGroupInfo: undefined,
getPrimaryMember: undefined,
} }
} }
default: { default: {
@@ -209,6 +222,9 @@ export class Convo {
markConvoAccepted: undefined, markConvoAccepted: undefined,
addReaction: undefined, addReaction: undefined,
removeReaction: undefined, removeReaction: undefined,
isGroup: this.isGroup,
getGroupInfo: this.getGroupInfo,
getPrimaryMember: this.getPrimaryMember,
} }
} }
} }
@@ -1332,4 +1348,34 @@ export class Convo {
throw error throw error
} }
} }
// Group utilities
isGroup(): boolean | undefined {
if (!this.convo) return undefined
return this.convo.kind === 'group'
}
getGroupInfo(): ChatBskyConvoDefs.GroupConvoData | undefined {
if (
this.convo &&
bsky.dangerousIsType<ChatBskyConvoDefs.GroupConvoData>(
this.convo.kindData,
ChatBskyConvoDefs.isGroupConvoData,
)
) {
return this.convo.kindData
}
return undefined
}
getPrimaryMember(): ChatBskyActorDefs.ProfileViewBasic | undefined {
if (this.convo?.kind === 'group') {
return (
this.recipients?.find(r => r.role === 'owner') ?? this.recipients?.[0]
)
} else {
return this.recipients?.find(r => r.did !== this.senderUserDid)
}
}
} }
+24
View File
@@ -144,6 +144,9 @@ type FetchMessageHistory = () => Promise<void>
type MarkConvoAccepted = () => void type MarkConvoAccepted = () => void
type AddReaction = (messageId: string, reaction: string) => Promise<void> type AddReaction = (messageId: string, reaction: string) => Promise<void>
type RemoveReaction = (messageId: string, reaction: string) => Promise<void> type RemoveReaction = (messageId: string, reaction: string) => Promise<void>
type IsGroup = () => boolean | undefined
type GetGroupInfo = () => ChatBskyConvoDefs.GroupConvoData | undefined
type GetPrimaryMember = () => ChatBskyActorDefs.ProfileViewBasic | undefined
export type ConvoStateUninitialized = { export type ConvoStateUninitialized = {
status: ConvoStatus.Uninitialized status: ConvoStatus.Uninitialized
@@ -159,6 +162,9 @@ export type ConvoStateUninitialized = {
markConvoAccepted: undefined markConvoAccepted: undefined
addReaction: undefined addReaction: undefined
removeReaction: undefined removeReaction: undefined
isGroup: IsGroup
getGroupInfo: GetGroupInfo
getPrimaryMember: GetPrimaryMember
} }
export type ConvoStateInitializing = { export type ConvoStateInitializing = {
status: ConvoStatus.Initializing status: ConvoStatus.Initializing
@@ -174,6 +180,9 @@ export type ConvoStateInitializing = {
markConvoAccepted: undefined markConvoAccepted: undefined
addReaction: undefined addReaction: undefined
removeReaction: undefined removeReaction: undefined
isGroup: IsGroup
getGroupInfo: GetGroupInfo
getPrimaryMember: GetPrimaryMember
} }
export type ConvoStateReady = { export type ConvoStateReady = {
status: ConvoStatus.Ready status: ConvoStatus.Ready
@@ -189,6 +198,9 @@ export type ConvoStateReady = {
markConvoAccepted: MarkConvoAccepted markConvoAccepted: MarkConvoAccepted
addReaction: AddReaction addReaction: AddReaction
removeReaction: RemoveReaction removeReaction: RemoveReaction
isGroup: IsGroup
getGroupInfo: GetGroupInfo
getPrimaryMember: GetPrimaryMember
} }
export type ConvoStateBackgrounded = { export type ConvoStateBackgrounded = {
status: ConvoStatus.Backgrounded status: ConvoStatus.Backgrounded
@@ -204,6 +216,9 @@ export type ConvoStateBackgrounded = {
markConvoAccepted: MarkConvoAccepted markConvoAccepted: MarkConvoAccepted
addReaction: AddReaction addReaction: AddReaction
removeReaction: RemoveReaction removeReaction: RemoveReaction
isGroup: IsGroup
getGroupInfo: GetGroupInfo
getPrimaryMember: GetPrimaryMember
} }
export type ConvoStateSuspended = { export type ConvoStateSuspended = {
status: ConvoStatus.Suspended status: ConvoStatus.Suspended
@@ -219,6 +234,9 @@ export type ConvoStateSuspended = {
markConvoAccepted: MarkConvoAccepted markConvoAccepted: MarkConvoAccepted
addReaction: AddReaction addReaction: AddReaction
removeReaction: RemoveReaction removeReaction: RemoveReaction
isGroup: IsGroup
getGroupInfo: GetGroupInfo
getPrimaryMember: GetPrimaryMember
} }
export type ConvoStateError = { export type ConvoStateError = {
status: ConvoStatus.Error status: ConvoStatus.Error
@@ -234,6 +252,9 @@ export type ConvoStateError = {
markConvoAccepted: undefined markConvoAccepted: undefined
addReaction: undefined addReaction: undefined
removeReaction: undefined removeReaction: undefined
isGroup: undefined
getGroupInfo: undefined
getPrimaryMember: undefined
} }
export type ConvoStateDisabled = { export type ConvoStateDisabled = {
status: ConvoStatus.Disabled status: ConvoStatus.Disabled
@@ -249,6 +270,9 @@ export type ConvoStateDisabled = {
markConvoAccepted: MarkConvoAccepted markConvoAccepted: MarkConvoAccepted
addReaction: AddReaction addReaction: AddReaction
removeReaction: RemoveReaction removeReaction: RemoveReaction
isGroup: IsGroup
getGroupInfo: GetGroupInfo
getPrimaryMember: GetPrimaryMember
} }
export type ConvoState = export type ConvoState =
| ConvoStateUninitialized | ConvoStateUninitialized