* Refactor ChatListItem for mod state * Refactor Conversation Header for mod state * Invalidate query for list when blocking/unblocking * Remove unused prop, restore border * Add mutations, hook up profile shadow to list query, use shadow-aware query for convo (#4024)
250 lines
7.9 KiB
TypeScript
250 lines
7.9 KiB
TypeScript
import React from 'react'
|
|
import {View} from 'react-native'
|
|
import {
|
|
AppBskyActorDefs,
|
|
ChatBskyConvoDefs,
|
|
moderateProfile,
|
|
ModerationOpts,
|
|
} from '@atproto/api'
|
|
import {msg} from '@lingui/macro'
|
|
import {useLingui} from '@lingui/react'
|
|
import {useNavigation} from '@react-navigation/native'
|
|
|
|
import {NavigationProp} from '#/lib/routes/types'
|
|
import {isNative} from '#/platform/detection'
|
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
|
import {useSession} from '#/state/session'
|
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
|
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
|
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
|
import {Button} from '#/components/Button'
|
|
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
|
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
|
|
import {useMenuControl} from '#/components/Menu'
|
|
import {Text} from '#/components/Typography'
|
|
|
|
export function ChatListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
|
|
const {currentAccount} = useSession()
|
|
const otherUser = convo.members.find(
|
|
member => member.did !== currentAccount?.did,
|
|
)
|
|
const moderationOpts = useModerationOpts()
|
|
|
|
if (!otherUser || !moderationOpts) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<ChatListItemReady
|
|
convo={convo}
|
|
profile={otherUser}
|
|
moderationOpts={moderationOpts}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function ChatListItemReady({
|
|
convo,
|
|
profile: profileUnshadowed,
|
|
moderationOpts,
|
|
}: {
|
|
convo: ChatBskyConvoDefs.ConvoView
|
|
profile: AppBskyActorDefs.ProfileViewBasic
|
|
moderationOpts: ModerationOpts
|
|
}) {
|
|
const t = useTheme()
|
|
const {_} = useLingui()
|
|
const {currentAccount} = useSession()
|
|
const menuControl = useMenuControl()
|
|
const {gtMobile} = useBreakpoints()
|
|
const profile = useProfileShadow(profileUnshadowed)
|
|
const moderation = React.useMemo(
|
|
() => moderateProfile(profile, moderationOpts),
|
|
[profile, moderationOpts],
|
|
)
|
|
|
|
const isDeletedAccount = profile.handle === 'missing.invalid'
|
|
const displayName = isDeletedAccount
|
|
? 'Deleted Account'
|
|
: sanitizeDisplayName(
|
|
profile.displayName || profile.handle,
|
|
moderation.ui('displayName'),
|
|
)
|
|
|
|
let lastMessage = _(msg`No messages yet`)
|
|
let lastMessageSentAt: string | null = null
|
|
if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
|
|
if (convo.lastMessage.sender?.did === currentAccount?.did) {
|
|
lastMessage = _(msg`You: ${convo.lastMessage.text}`)
|
|
} else {
|
|
lastMessage = convo.lastMessage.text
|
|
}
|
|
lastMessageSentAt = convo.lastMessage.sentAt
|
|
}
|
|
if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) {
|
|
lastMessage = _(msg`Message deleted`)
|
|
}
|
|
|
|
const navigation = useNavigation<NavigationProp>()
|
|
const [showActions, setShowActions] = React.useState(false)
|
|
|
|
const onMouseEnter = React.useCallback(() => {
|
|
setShowActions(true)
|
|
}, [])
|
|
|
|
const onMouseLeave = React.useCallback(() => {
|
|
setShowActions(false)
|
|
}, [])
|
|
|
|
const onFocus = React.useCallback<React.FocusEventHandler>(e => {
|
|
if (e.nativeEvent.relatedTarget == null) return
|
|
setShowActions(true)
|
|
}, [])
|
|
|
|
const onPress = React.useCallback(() => {
|
|
navigation.push('MessagesConversation', {
|
|
conversation: convo.id,
|
|
})
|
|
}, [convo.id, navigation])
|
|
|
|
return (
|
|
<View
|
|
// @ts-expect-error web only
|
|
onMouseEnter={onMouseEnter}
|
|
onMouseLeave={onMouseLeave}
|
|
onFocus={onFocus}
|
|
onBlur={onMouseLeave}>
|
|
<Button
|
|
label={profile.displayName || profile.handle}
|
|
onPress={onPress}
|
|
style={a.flex_1}
|
|
onLongPress={isNative ? menuControl.open : undefined}>
|
|
{({hovered, pressed}) => (
|
|
<View
|
|
style={[
|
|
a.flex_row,
|
|
a.flex_1,
|
|
a.px_lg,
|
|
a.py_md,
|
|
a.gap_md,
|
|
(hovered || pressed) && t.atoms.bg_contrast_25,
|
|
t.atoms.border_contrast_low,
|
|
]}>
|
|
<UserAvatar
|
|
avatar={profile.avatar}
|
|
size={52}
|
|
moderation={moderation.ui('avatar')}
|
|
/>
|
|
<View style={[a.flex_1, a.flex_row, a.align_center]}>
|
|
<View style={[a.flex_1]}>
|
|
<View
|
|
style={[
|
|
a.flex_1,
|
|
a.flex_row,
|
|
a.align_end,
|
|
a.pb_2xs,
|
|
web([{marginTop: -2}]),
|
|
]}>
|
|
<Text
|
|
numberOfLines={1}
|
|
style={[{maxWidth: '85%'}, web([a.leading_normal])]}>
|
|
<Text style={[a.text_md, t.atoms.text, a.font_bold]}>
|
|
{displayName}
|
|
</Text>
|
|
</Text>
|
|
{lastMessageSentAt && (
|
|
<TimeElapsed timestamp={lastMessageSentAt}>
|
|
{({timeElapsed}) => (
|
|
<Text
|
|
style={[
|
|
a.text_sm,
|
|
web([a.leading_normal, {whiteSpace: 'pre'}]),
|
|
t.atoms.text_contrast_medium,
|
|
]}>
|
|
{' '}
|
|
· {timeElapsed}
|
|
</Text>
|
|
)}
|
|
</TimeElapsed>
|
|
)}
|
|
{convo.muted && (
|
|
<Text
|
|
style={[
|
|
a.text_sm,
|
|
web([a.leading_normal, {whiteSpace: 'pre'}]),
|
|
t.atoms.text_contrast_medium,
|
|
]}>
|
|
{' '}
|
|
·{' '}
|
|
<BellStroke
|
|
size="xs"
|
|
style={t.atoms.text_contrast_medium}
|
|
/>
|
|
</Text>
|
|
)}
|
|
</View>
|
|
{!isDeletedAccount && (
|
|
<Text
|
|
numberOfLines={1}
|
|
style={[a.text_sm, t.atoms.text_contrast_medium, a.pb_xs]}>
|
|
@{profile.handle}
|
|
</Text>
|
|
)}
|
|
<Text
|
|
numberOfLines={2}
|
|
style={[
|
|
a.text_sm,
|
|
a.leading_snug,
|
|
convo.unreadCount > 0
|
|
? a.font_bold
|
|
: t.atoms.text_contrast_high,
|
|
convo.muted && t.atoms.text_contrast_medium,
|
|
]}>
|
|
{lastMessage}
|
|
</Text>
|
|
</View>
|
|
{convo.unreadCount > 0 && (
|
|
<View
|
|
style={[
|
|
a.absolute,
|
|
a.rounded_full,
|
|
{
|
|
backgroundColor: convo.muted
|
|
? t.palette.contrast_200
|
|
: t.palette.primary_500,
|
|
height: 7,
|
|
width: 7,
|
|
},
|
|
isNative
|
|
? {
|
|
top: 15,
|
|
right: 12,
|
|
}
|
|
: {
|
|
top: 0,
|
|
right: 0,
|
|
},
|
|
]}
|
|
/>
|
|
)}
|
|
<ConvoMenu
|
|
convo={convo}
|
|
profile={profile}
|
|
control={menuControl}
|
|
currentScreen="list"
|
|
showMarkAsRead={convo.unreadCount > 0}
|
|
hideTrigger={isNative}
|
|
triggerOpacity={
|
|
!gtMobile || showActions || menuControl.isOpen ? 1 : 0
|
|
}
|
|
moderation={moderation}
|
|
/>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</Button>
|
|
</View>
|
|
)
|
|
}
|