diff options
author | Samuel Newman <mozzius@protonmail.com> | 2024-10-02 22:21:59 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-10-02 22:21:59 +0300 |
commit | 13c9c79aeec77edc33b1a926843b005c14acccc7 (patch) | |
tree | 0ce80c052a7e504c99e842f0ba6a0f1f2f379a1e /src/screens/Messages/components/ChatListItem.tsx | |
parent | 405966830ccdbee6152037eebb76c4815ff5526c (diff) | |
download | voidsky-13c9c79aeec77edc33b1a926843b005c14acccc7.tar.zst |
move files around (#5576)
Diffstat (limited to 'src/screens/Messages/components/ChatListItem.tsx')
-rw-r--r-- | src/screens/Messages/components/ChatListItem.tsx | 378 |
1 files changed, 378 insertions, 0 deletions
diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx new file mode 100644 index 000000000..11c071082 --- /dev/null +++ b/src/screens/Messages/components/ChatListItem.tsx @@ -0,0 +1,378 @@ +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 {useHaptics} from '#/lib/haptics' +import {decrementBadgeCount} from '#/lib/notifications/notifications' +import {logEvent} from '#/lib/statsig/statsig' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +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 {TimeElapsed} from '#/view/com/util/TimeElapsed' +import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import * as tokens from '#/alf/tokens' +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]}> + <View + style={[ + a.z_10, + a.absolute, + {top: tokens.space.md, left: tokens.space.lg}, + ]}> + <PreviewableUserAvatar + profile={profile} + size={52} + moderation={moderation.ui('avatar')} + /> + </View> + + <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, + ]}> + {/* Avatar goes here */} + <View style={{width: 52, height: 52}} /> + + <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 + emoji + 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 + emoji + 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="lg" + 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: tokens.space.lg, + opacity: !gtMobile || showActions || menuControl.isOpen ? 1 : 0, + }, + ]} + /> + </View> + ) +} |