Files
bsky-social-app/src/view/com/composer/ComposerReplyTo.tsx
T
Eric Bailey 0ac15920a4 Verification (#8226)
* WIP

* Alignment with icon

* Add create/remove prompts

* Fill out check dialog a bit

* Reorg

* Handle was verified state

* Add warning to edit profile

* Add warning to handle dialog

* Decent alignment in posts on all platforms

* Refactor alignment for posts, chatlist, hover card

* Disable on profile

* Convo header

* Compute simple verification state

* Add other icon, rename, integrate

* Swap in simple state for profile edits

* Clean up utility hooks

* Add verifications UI to dialog

* Add edu nux

* Revert change

* Fix wrapping of check on profile

* Rename

* Fix gap under PostMeta

* Update check dialogs

* Handle takendown verifiers in check dialog

* alf composer reply to

* Refactor verification state

* Add create/remove mutations, non-functional for now

* Fix up post-rebase

* Add check to first author noty

* Do cache updates after mutations

* DRY up hook, add to profile updates too

* Add to drawer

* Update account list

* Adapt to new types

* Hook up mutations

* Use profile shadow in feeds

* Add to settings

* Shadow currentAccountProfile

* Add invalid state to verifications

* Fix alignment and overflow in Settings and Drawer

* Re-integrate post rebase

* Remove debug code

* Update copy

* Add unverified notification support

* Remove link

* Make sure dialog closes

* Update URL

* Add settings screen

* Integrate new setting into verification states

* Add metrics, bump package, fix bad import

* NUX fixes

* Update copy

* Fixes

* Update types

* fix search autocomplete

* fix lint

* add display name warning to new dialog

* update default prefs

* Add parsing support for notifications

* Bump pkg

* Tweak noty styles

* Adjust check alignment

* Tweak check alignment

* Fix badge for verifier

* Modify copy

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2025-04-18 19:15:32 -07:00

233 lines
6.9 KiB
TypeScript

import {useCallback, useMemo, useState} from 'react'
import {LayoutAnimation, Pressable, View} from 'react-native'
import {Image} from 'expo-image'
import {
AppBskyEmbedImages,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedPost,
} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {type ComposerOptsPostRef} from '#/state/shell/composer'
import {MaybeQuoteEmbed} from '#/view/com/util/post-embeds/QuoteEmbed'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
const t = useTheme()
const {_} = useLingui()
const {embed} = replyTo
const [showFull, setShowFull] = useState(false)
const onPress = useCallback(() => {
setShowFull(prev => !prev)
LayoutAnimation.configureNext({
duration: 350,
update: {type: 'spring', springDamping: 0.7},
})
}, [])
const quoteEmbed = useMemo(() => {
if (
AppBskyEmbedRecord.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record) &&
AppBskyFeedPost.isRecord(embed.record.value)
) {
return embed
} else if (
AppBskyEmbedRecordWithMedia.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
AppBskyFeedPost.isRecord(embed.record.record.value)
) {
return embed.record
}
return null
}, [embed])
const images = useMemo(() => {
if (AppBskyEmbedImages.isView(embed)) {
return embed.images
} else if (
AppBskyEmbedRecordWithMedia.isView(embed) &&
AppBskyEmbedImages.isView(embed.media)
) {
return embed.media.images
}
}, [embed])
const verification = useSimpleVerificationState({profile: replyTo.author})
return (
<Pressable
style={[
a.flex_row,
a.align_start,
a.pt_xs,
a.pb_lg,
a.mb_md,
a.mx_lg,
a.border_b,
t.atoms.border_contrast_medium,
]}
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={_(
msg`Expand or collapse the full post you are replying to`,
)}
accessibilityHint="">
<PreviewableUserAvatar
size={50}
profile={replyTo.author}
moderation={replyTo.moderation?.ui('avatar')}
type={replyTo.author.associated?.labeler ? 'labeler' : 'user'}
disableNavigation={true}
/>
<View style={[a.flex_1, a.pl_md, a.pr_sm, a.gap_2xs]}>
<View style={[a.flex_row, a.align_center, a.pr_xs]}>
<Text
style={[a.font_bold, a.text_md, a.flex_shrink]}
numberOfLines={1}
emoji>
{sanitizeDisplayName(
replyTo.author.displayName ||
sanitizeHandle(replyTo.author.handle),
)}
</Text>
{verification.showBadge && (
<View style={[a.pl_xs]}>
<VerificationCheck
width={14}
verifier={verification.role === 'verifier'}
/>
</View>
)}
</View>
<View style={[a.flex_row, a.gap_md]}>
<View style={[a.flex_1, a.flex_grow]}>
<Text
style={[a.text_md]}
numberOfLines={!showFull ? 6 : undefined}
emoji>
{replyTo.text}
</Text>
</View>
{images && !replyTo.moderation?.ui('contentMedia').blur && (
<ComposerReplyToImages images={images} showFull={showFull} />
)}
</View>
{showFull && quoteEmbed && <MaybeQuoteEmbed embed={quoteEmbed} />}
</View>
</Pressable>
)
}
function ComposerReplyToImages({
images,
}: {
images: AppBskyEmbedImages.ViewImage[]
showFull: boolean
}) {
return (
<View
style={[
a.rounded_xs,
a.overflow_hidden,
a.mt_2xs,
a.mx_xs,
{
height: 64,
width: 64,
},
]}>
{(images.length === 1 && (
<Image
source={{uri: images[0].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
)) ||
(images.length === 2 && (
<View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
<Image
source={{uri: images[0].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[1].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
)) ||
(images.length === 3 && (
<View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
<Image
source={{uri: images[0].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<View style={[a.flex_1, a.gap_2xs]}>
<Image
source={{uri: images[1].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[2].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
</View>
)) ||
(images.length === 4 && (
<View style={[a.flex_1, a.gap_2xs]}>
<View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
<Image
source={{uri: images[0].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[1].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
<View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
<Image
source={{uri: images[2].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[3].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
</View>
))}
</View>
)
}