* register the push token after request has been approved on android * use a `0` string for badge count * Revert "use a `0` string for badge count" This reverts commit efac39861817f4237c58211f68ef266d919b4d40. * temporary fix
365 lines
11 KiB
TypeScript
365 lines
11 KiB
TypeScript
import React, {useCallback, useState} from 'react'
|
|
import {GestureResponderEvent, View} from 'react-native'
|
|
import {
|
|
AppBskyActorDefs,
|
|
AppBskyEmbedRecord,
|
|
ChatBskyConvoDefs,
|
|
moderateProfile,
|
|
ModerationOpts,
|
|
} from '@atproto/api'
|
|
import {msg} from '@lingui/macro'
|
|
import {useLingui} from '@lingui/react'
|
|
|
|
import {
|
|
postUriToRelativePath,
|
|
toBskyAppUrl,
|
|
toShortUrl,
|
|
} from '#/lib/strings/url-helpers'
|
|
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 {useHaptics} from 'lib/haptics'
|
|
import {decrementBadgeCount} from 'lib/notifications/notifications'
|
|
import {logEvent} from 'lib/statsig/statsig'
|
|
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 {ConvoMenu} from '#/components/dms/ConvoMenu'
|
|
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
|
|
import {Link} from '#/components/Link'
|
|
import {useMenuControl} from '#/components/Menu'
|
|
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
|
import {Text} from '#/components/Typography'
|
|
|
|
export let ChatListItem = ({
|
|
convo,
|
|
}: {
|
|
convo: ChatBskyConvoDefs.ConvoView
|
|
}): React.ReactNode => {
|
|
const {currentAccount} = useSession()
|
|
const moderationOpts = useModerationOpts()
|
|
|
|
const otherUser = convo.members.find(
|
|
member => member.did !== currentAccount?.did,
|
|
)
|
|
|
|
if (!otherUser || !moderationOpts) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<ChatListItemReady
|
|
convo={convo}
|
|
profile={otherUser}
|
|
moderationOpts={moderationOpts}
|
|
/>
|
|
)
|
|
}
|
|
|
|
ChatListItem = React.memo(ChatListItem)
|
|
|
|
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 playHaptic = useHaptics()
|
|
|
|
const blockInfo = React.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
|
|
? 'Deleted Account'
|
|
: sanitizeDisplayName(
|
|
profile.displayName || profile.handle,
|
|
moderation.ui('displayName'),
|
|
)
|
|
|
|
const isDimStyle = convo.muted || moderation.blocked || isDeletedAccount
|
|
|
|
const {lastMessage, lastMessageSentAt} = React.useMemo(() => {
|
|
let lastMessage = _(msg`No messages yet`)
|
|
let lastMessageSentAt: string | null = null
|
|
|
|
if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
|
|
const isFromMe = convo.lastMessage.sender?.did === currentAccount?.did
|
|
|
|
if (convo.lastMessage.text) {
|
|
if (isFromMe) {
|
|
lastMessage = _(msg`You: ${convo.lastMessage.text}`)
|
|
} else {
|
|
lastMessage = convo.lastMessage.text
|
|
}
|
|
} else if (convo.lastMessage.embed) {
|
|
const defaultEmbeddedContentMessage = _(
|
|
msg`(contains embedded content)`,
|
|
)
|
|
|
|
if (AppBskyEmbedRecord.isView(convo.lastMessage.embed)) {
|
|
const embed = convo.lastMessage.embed
|
|
|
|
if (AppBskyEmbedRecord.isViewRecord(embed.record)) {
|
|
const record = embed.record
|
|
const path = postUriToRelativePath(record.uri, {
|
|
handle: record.author.handle,
|
|
})
|
|
const href = path ? toBskyAppUrl(path) : undefined
|
|
const short = href
|
|
? toShortUrl(href)
|
|
: defaultEmbeddedContentMessage
|
|
if (isFromMe) {
|
|
lastMessage = _(msg`You: ${short}`)
|
|
} else {
|
|
lastMessage = short
|
|
}
|
|
}
|
|
} else {
|
|
if (isFromMe) {
|
|
lastMessage = _(msg`You: ${defaultEmbeddedContentMessage}`)
|
|
} else {
|
|
lastMessage = defaultEmbeddedContentMessage
|
|
}
|
|
}
|
|
}
|
|
|
|
lastMessageSentAt = convo.lastMessage.sentAt
|
|
}
|
|
if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) {
|
|
lastMessage = isDeletedAccount
|
|
? _(msg`Conversation deleted`)
|
|
: _(msg`Message deleted`)
|
|
}
|
|
|
|
return {
|
|
lastMessage,
|
|
lastMessageSentAt,
|
|
}
|
|
}, [_, convo.lastMessage, currentAccount?.did, isDeletedAccount])
|
|
|
|
const [showActions, setShowActions] = useState(false)
|
|
|
|
const onMouseEnter = useCallback(() => {
|
|
setShowActions(true)
|
|
}, [])
|
|
|
|
const onMouseLeave = useCallback(() => {
|
|
setShowActions(false)
|
|
}, [])
|
|
|
|
const onFocus = useCallback<React.FocusEventHandler>(e => {
|
|
if (e.nativeEvent.relatedTarget == null) return
|
|
setShowActions(true)
|
|
}, [])
|
|
|
|
const onPress = useCallback(
|
|
(e: GestureResponderEvent) => {
|
|
decrementBadgeCount(convo.unreadCount)
|
|
if (isDeletedAccount) {
|
|
e.preventDefault()
|
|
menuControl.open()
|
|
return false
|
|
} else {
|
|
logEvent('chat:open', {logContext: 'ChatsList'})
|
|
}
|
|
},
|
|
[convo.unreadCount, isDeletedAccount, menuControl],
|
|
)
|
|
|
|
const onLongPress = useCallback(() => {
|
|
playHaptic()
|
|
menuControl.open()
|
|
}, [playHaptic, menuControl])
|
|
|
|
return (
|
|
<View
|
|
// @ts-expect-error web only
|
|
onMouseEnter={onMouseEnter}
|
|
onMouseLeave={onMouseLeave}
|
|
onFocus={onFocus}
|
|
onBlur={onMouseLeave}
|
|
style={[a.relative]}>
|
|
<Link
|
|
to={`/messages/${convo.id}`}
|
|
label={displayName}
|
|
accessibilityHint={
|
|
!isDeletedAccount
|
|
? _(msg`Go to conversation with ${profile.handle}`)
|
|
: _(
|
|
msg`This conversation is with a deleted or a deactivated account. Press for options.`,
|
|
)
|
|
}
|
|
accessibilityActions={
|
|
isNative
|
|
? [
|
|
{name: 'magicTap', label: _(msg`Open conversation options`)},
|
|
{name: 'longpress', label: _(msg`Open conversation options`)},
|
|
]
|
|
: undefined
|
|
}
|
|
onPress={onPress}
|
|
onLongPress={isNative ? onLongPress : undefined}
|
|
onAccessibilityAction={onLongPress}>
|
|
{({hovered, pressed, focused}) => (
|
|
<View
|
|
style={[
|
|
a.flex_row,
|
|
isDeletedAccount ? a.align_center : a.align_start,
|
|
a.flex_1,
|
|
a.px_lg,
|
|
a.py_md,
|
|
a.gap_md,
|
|
(hovered || pressed || focused) && 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.justify_center, web({paddingRight: 45})]}>
|
|
<View style={[a.w_full, a.flex_row, a.align_end, a.pb_2xs]}>
|
|
<Text
|
|
numberOfLines={1}
|
|
style={[{maxWidth: '85%'}, web([a.leading_normal])]}>
|
|
<Text
|
|
style={[
|
|
a.text_md,
|
|
t.atoms.text,
|
|
a.font_bold,
|
|
{lineHeight: 21},
|
|
isDimStyle && t.atoms.text_contrast_medium,
|
|
]}>
|
|
{displayName}
|
|
</Text>
|
|
</Text>
|
|
{lastMessageSentAt && (
|
|
<TimeElapsed timestamp={lastMessageSentAt}>
|
|
{({timeElapsed}) => (
|
|
<Text
|
|
style={[
|
|
a.text_sm,
|
|
{lineHeight: 21},
|
|
t.atoms.text_contrast_medium,
|
|
web({whiteSpace: 'preserve nowrap'}),
|
|
]}>
|
|
{' '}
|
|
· {timeElapsed}
|
|
</Text>
|
|
)}
|
|
</TimeElapsed>
|
|
)}
|
|
{(convo.muted || moderation.blocked) && (
|
|
<Text
|
|
style={[
|
|
a.text_sm,
|
|
{lineHeight: 21},
|
|
t.atoms.text_contrast_medium,
|
|
web({whiteSpace: 'preserve nowrap'}),
|
|
]}>
|
|
{' '}
|
|
·{' '}
|
|
<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,
|
|
isDimStyle && t.atoms.text_contrast_medium,
|
|
]}>
|
|
{lastMessage}
|
|
</Text>
|
|
|
|
<PostAlerts
|
|
modui={moderation.ui('contentList')}
|
|
size="large"
|
|
style={[a.pt_xs]}
|
|
/>
|
|
</View>
|
|
|
|
{convo.unreadCount > 0 && (
|
|
<View
|
|
style={[
|
|
a.absolute,
|
|
a.rounded_full,
|
|
{
|
|
backgroundColor: isDimStyle
|
|
? t.palette.contrast_200
|
|
: t.palette.primary_500,
|
|
height: 7,
|
|
width: 7,
|
|
top: 15,
|
|
right: 12,
|
|
},
|
|
]}
|
|
/>
|
|
)}
|
|
</View>
|
|
)}
|
|
</Link>
|
|
|
|
<ConvoMenu
|
|
convo={convo}
|
|
profile={profile}
|
|
control={menuControl}
|
|
currentScreen="list"
|
|
showMarkAsRead={convo.unreadCount > 0}
|
|
hideTrigger={isNative}
|
|
blockInfo={blockInfo}
|
|
style={[
|
|
a.absolute,
|
|
a.h_full,
|
|
a.self_end,
|
|
a.justify_center,
|
|
{
|
|
right: a.px_lg.paddingRight,
|
|
opacity: !gtMobile || showActions || menuControl.isOpen ? 1 : 0,
|
|
},
|
|
]}
|
|
/>
|
|
</View>
|
|
)
|
|
}
|