Make optimistic deleted state not look like crap in tree view

This commit is contained in:
Eric Bailey
2025-06-04 19:47:34 -05:00
parent de2bcaac2e
commit 25ee588e27
+217 -161
View File
@@ -66,7 +66,7 @@ export function ThreadReply({
const postShadow = usePostShadow(item.value.post) const postShadow = usePostShadow(item.value.post)
if (postShadow === POST_TOMBSTONE) { if (postShadow === POST_TOMBSTONE) {
return <PostThreadItemDeleted /> return <ThreadItemTreeReplyDeleted item={item} />
} }
return ( return (
@@ -82,26 +82,157 @@ export function ThreadReply({
) )
} }
function PostThreadItemDeleted({hideTopBorder}: {hideTopBorder?: boolean}) { function ThreadItemTreeReplyDeleted({
item,
}: {
item: Extract<ThreadItem, {type: 'threadPost'}>
}) {
const t = useTheme()
return (
<ThreadItemTreeReplyOuter item={item}>
<ThreadItemTreeReplyInner item={item}>
<View
style={[
a.flex_row,
a.align_center,
a.rounded_sm,
t.atoms.bg_contrast_25,
{
gap: 6,
paddingHorizontal: OUTER_SPACE / 2,
height: TREE_AVI_WIDTH,
},
]}>
<TrashIcon style={[t.atoms.text]} width={14} />
<Text style={[t.atoms.text_contrast_medium, a.mt_2xs]}>
<Trans>This post has been deleted.</Trans>
</Text>
</View>
{item.ui.isLastChild && !item.ui.precedesChildReadMore && (
<View style={{height: OUTER_SPACE / 2}} />
)}
</ThreadItemTreeReplyInner>
</ThreadItemTreeReplyOuter>
)
}
const ThreadItemTreeReplyOuter = memo(function ThreadItemTreeReplyOuter({
item,
children,
}: {
item: Extract<ThreadItem, {type: 'threadPost'}>
children: React.ReactNode
}) {
const t = useTheme()
const indents = Math.max(0, item.ui.indent - 1)
return (
<View
style={[
a.flex_row,
item.ui.indent === 1 &&
!item.ui.showParentReplyLine && [
a.border_t,
t.atoms.border_contrast_low,
],
]}>
{Array.from(Array(indents)).map((_, n: number) => {
const isSkipped = item.ui.skippedIndentIndices.has(n)
return (
<View
key={`${item.value.post.uri}-padding-${n}`}
style={[
t.atoms.border_contrast_low,
{
borderRightWidth: isSkipped ? 0 : REPLY_LINE_WIDTH,
width: TREE_INDENT + TREE_AVI_WIDTH / 2,
left: 1,
},
]}
/>
)
})}
{children}
</View>
)
})
const ThreadItemTreeReplyInner = memo(function ThreadItemTreeReplyInner({
item,
children,
}: {
item: Extract<ThreadItem, {type: 'threadPost'}>
children: React.ReactNode
}) {
const t = useTheme() const t = useTheme()
return ( return (
<View <View
style={[ style={[
t.atoms.bg, a.flex_1, // TODO check on ios
t.atoms.border_contrast_low, {
a.p_xl, paddingHorizontal: OUTER_SPACE,
a.pl_lg, paddingTop: OUTER_SPACE / 2,
a.flex_row, },
a.gap_md, item.ui.indent === 1 && [
!hideTopBorder && a.border_t, !item.ui.showParentReplyLine && a.pt_lg,
!item.ui.showChildReplyLine && a.pb_sm,
],
item.ui.isLastChild &&
!item.ui.precedesChildReadMore && [
{
paddingBottom: OUTER_SPACE / 2,
},
],
]}> ]}>
<TrashIcon style={[t.atoms.text]} /> {item.ui.indent > 1 && (
<Text style={[t.atoms.text_contrast_medium, a.mt_2xs]}> <View
<Trans>This post has been deleted.</Trans> style={[
</Text> a.absolute,
t.atoms.border_contrast_low,
{
left: -1,
top: 0,
height:
TREE_AVI_WIDTH / 2 + REPLY_LINE_WIDTH / 2 + OUTER_SPACE / 2,
width: OUTER_SPACE,
borderLeftWidth: REPLY_LINE_WIDTH,
borderBottomWidth: REPLY_LINE_WIDTH,
borderBottomLeftRadius: a.rounded_sm.borderRadius,
},
]}
/>
)}
{children}
</View> </View>
) )
} })
const ThreadItemTreeReplyChildReplyLine = memo(
function ThreadItemTreeReplyChildReplyLine({
item,
}: {
item: Extract<ThreadItem, {type: 'threadPost'}>
}) {
const t = useTheme()
return (
<View style={[a.relative, {width: TREE_AVI_PLUS_SPACE}]}>
{item.ui.showChildReplyLine && (
<View
style={[
a.flex_1,
t.atoms.border_contrast_low,
{
borderRightWidth: 2,
width: '50%',
left: -1,
},
]}
/>
)}
</View>
)
},
)
const ThreadReplyInner = memo(function ThreadReplyInner({ const ThreadReplyInner = memo(function ThreadReplyInner({
item, item,
@@ -119,7 +250,6 @@ const ThreadReplyInner = memo(function ThreadReplyInner({
onPostSuccess?: (data: OnPostSuccessData) => void onPostSuccess?: (data: OnPostSuccessData) => void
threadgateRecord?: AppBskyFeedThreadgate.Record threadgateRecord?: AppBskyFeedThreadgate.Record
}): React.ReactNode { }): React.ReactNode {
const t = useTheme()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {openComposer} = useOpenComposer() const {openComposer} = useOpenComposer()
@@ -180,160 +310,86 @@ const ThreadReplyInner = memo(function ThreadReplyInner({
setLimitLines(false) setLimitLines(false)
}, [setLimitLines]) }, [setLimitLines])
const indents = Math.max(0, item.ui.indent - 1)
return ( return (
<View <ThreadItemTreeReplyOuter item={item}>
style={[ <SubtleHover>
a.flex_row, <PostHider
item.ui.indent === 1 && testID={`postThreadItem-by-${post.author.handle}`}
!item.ui.showParentReplyLine && [ href={postHref}
a.border_t, disabled={overrides?.moderation === true}
t.atoms.border_contrast_low, modui={moderation.ui('contentList')}
], iconSize={42}
]}> iconStyles={{marginLeft: 2, marginRight: 2}}
{Array.from(Array(indents)).map((_, n: number) => { profile={post.author}
const isSkipped = item.ui.skippedIndentIndices.has(n) interpretFilterAsBlur>
return ( <ThreadItemTreeReplyInner item={item}>
<View <View style={[a.flex_row, a.gap_md]}>
key={`${post.uri}-padding-${n}`} <View style={[a.flex_1]}>
style={[ <PostMeta
t.atoms.border_contrast_low, author={post.author}
{ moderation={moderation}
borderRightWidth: isSkipped ? 0 : REPLY_LINE_WIDTH, timestamp={post.indexedAt}
width: TREE_INDENT + TREE_AVI_WIDTH / 2, postHref={postHref}
left: 1, avatarSize={TREE_AVI_WIDTH}
}, style={[a.pb_2xs]}
]} showAvatar
/>
)
})}
<View style={a.flex_1}>
<SubtleHover>
<PostHider
testID={`postThreadItem-by-${post.author.handle}`}
href={postHref}
disabled={overrides?.moderation === true}
modui={moderation.ui('contentList')}
iconSize={42}
iconStyles={{marginLeft: 2, marginRight: 2}}
profile={post.author}
interpretFilterAsBlur>
<View
style={[
{
paddingHorizontal: OUTER_SPACE,
paddingTop: OUTER_SPACE / 2,
},
item.ui.indent === 1 && [
!item.ui.showParentReplyLine && a.pt_lg,
!item.ui.showChildReplyLine && a.pb_sm,
],
item.ui.isLastChild &&
!item.ui.precedesChildReadMore && [a.pb_sm],
]}>
{item.ui.indent > 1 && (
<View
style={[
a.absolute,
t.atoms.border_contrast_low,
{
left: -1,
top: 0,
height:
TREE_AVI_WIDTH / 2 +
REPLY_LINE_WIDTH / 2 +
OUTER_SPACE / 2,
width: OUTER_SPACE,
borderLeftWidth: REPLY_LINE_WIDTH,
borderBottomWidth: REPLY_LINE_WIDTH,
borderBottomLeftRadius: a.rounded_sm.borderRadius,
},
]}
/> />
)} <View style={[a.flex_row]}>
<View style={[a.flex_row, a.gap_md]}> <ThreadItemTreeReplyChildReplyLine item={item} />
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
<PostMeta <LabelsOnMyPost post={post} style={[a.pb_2xs]} />
author={post.author} <PostAlerts
moderation={moderation} modui={moderation.ui('contentList')}
timestamp={post.indexedAt} style={[a.pb_2xs]}
postHref={postHref} additionalCauses={additionalPostAlerts}
avatarSize={TREE_AVI_WIDTH} />
style={[a.pb_2xs]} {richText?.text ? (
showAvatar <View>
/> <RichText
<View style={[a.flex_row]}> enableTags
<View style={[a.relative, {width: TREE_AVI_PLUS_SPACE}]}> value={richText}
{item.ui.showChildReplyLine && ( style={[a.flex_1, a.text_md]}
<View numberOfLines={
style={[ limitLines ? MAX_POST_LINES : undefined
a.flex_1, }
t.atoms.border_contrast_low, authorHandle={post.author.handle}
{ shouldProxyLinks={true}
borderRightWidth: 2,
width: '50%',
left: -1,
},
]}
/> />
)} </View>
</View> ) : undefined}
<View style={[a.flex_1]}> {limitLines ? (
<LabelsOnMyPost post={post} style={[a.pb_2xs]} /> <TextLink
<PostAlerts text={_(msg`Show More`)}
modui={moderation.ui('contentList')} style={pal.link}
style={[a.pb_2xs]} onPress={onPressShowMore}
additionalCauses={additionalPostAlerts} href="#"
/> />
{richText?.text ? ( ) : undefined}
<View> {post.embed && (
<RichText <View style={[a.pb_xs]}>
enableTags <PostEmbeds
value={richText} embed={post.embed}
style={[a.flex_1, a.text_md]} moderation={moderation}
numberOfLines={ viewContext={PostEmbedViewContext.Feed}
limitLines ? MAX_POST_LINES : undefined
}
authorHandle={post.author.handle}
shouldProxyLinks={true}
/>
</View>
) : undefined}
{limitLines ? (
<TextLink
text={_(msg`Show More`)}
style={pal.link}
onPress={onPressShowMore}
href="#"
/> />
) : undefined} </View>
{post.embed && ( )}
<View style={[a.pb_xs]}> <PostControls
<PostEmbeds post={postShadow}
embed={post.embed} record={record}
moderation={moderation} richText={richText}
viewContext={PostEmbedViewContext.Feed} onPressReply={onPressReply}
/> logContext="PostThreadItem"
</View> threadgateRecord={threadgateRecord}
)} />
<PostControls
post={postShadow}
record={record}
richText={richText}
onPressReply={onPressReply}
logContext="PostThreadItem"
threadgateRecord={threadgateRecord}
/>
</View>
</View> </View>
</View> </View>
</View> </View>
</View> </View>
</PostHider> </ThreadItemTreeReplyInner>
</SubtleHover> </PostHider>
</View> </SubtleHover>
</View> </ThreadItemTreeReplyOuter>
) )
}) })
@@ -347,7 +403,7 @@ function SubtleHover({children}: {children: React.ReactNode}) {
<View <View
onPointerEnter={onHoverIn} onPointerEnter={onHoverIn}
onPointerLeave={onHoverOut} onPointerLeave={onHoverOut}
style={a.pointer}> style={[a.flex_1, a.pointer]}>
<SubtleWebHover hover={hover} /> <SubtleWebHover hover={hover} />
{children} {children}
</View> </View>