Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add collapsable threads #7488

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/components/RichText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type RichTextProps = TextStyleProp &
enableTags?: boolean
authorHandle?: string
onLinkPress?: LinkProps['onPress']
onLinkLongPress?: LinkProps['onLongPress']
interactiveStyle?: TextStyle
emojiMultiplier?: number
}
Expand All @@ -35,6 +36,7 @@ export function RichText({
enableTags = false,
authorHandle,
onLinkPress,
onLinkLongPress,
interactiveStyle,
emojiMultiplier = 1.85,
onLayout,
Expand Down Expand Up @@ -110,7 +112,8 @@ export function RichText({
style={interactiveStyles}
// @ts-ignore TODO
dataSet={WORD_WRAP}
onPress={onLinkPress}>
onPress={onLinkPress}
onLongPress={onLinkLongPress}>
{segment.text}
</InlineLinkText>
</ProfileHoverCard>,
Expand Down
1 change: 1 addition & 0 deletions src/components/moderation/PostHider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export function PostHider({
setOverride(v => !v)
}
}}
onLongPress={props.onLongPress}
accessibilityRole="button"
accessibilityHint={
override ? _(msg`Hides the content`) : _(msg`Shows the content`)
Expand Down
27 changes: 27 additions & 0 deletions src/view/com/post-thread/PostThread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ const keyExtractor = (item: RowItem) => {
return item._reactKey
}

const NO_OP = () => {}

export function PostThread({uri}: {uri: string | undefined}) {
const {hasSession, currentAccount} = useSession()
const {_} = useLingui()
Expand Down Expand Up @@ -255,6 +257,28 @@ export function PostThread({uri}: {uri: string | undefined}) {
randomCache,
])

const [collapsedURIs, setCollapsedURIs] = React.useState(new Set())
const descendantsOfCollapsedURIs = React.useMemo(() => {
const set = new Set<string>()
if (!skeleton) return set

for (let val of skeleton.replies) {
if (!isThreadPost(val)) continue
if (collapsedURIs.has(val.post.uri) || set.has(val.post.uri)) {
val.replies?.forEach(r => {
isThreadPost(r) && set.add(r.post.uri)
})
}
}
return set
}, [collapsedURIs, skeleton])
const toggleCollapse = React.useCallback((collapseUri: string) => {
setCollapsedURIs(x => {
x.has(collapseUri) ? x.delete(collapseUri) : x.add(collapseUri)
return new Set(x)
})
}, [])

const error = React.useMemo(() => {
if (AppBskyFeedDefs.isNotFoundPost(thread)) {
return {
Expand Down Expand Up @@ -487,6 +511,9 @@ export function PostThread({uri}: {uri: string | undefined}) {
ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
onLayout={deferParents ? () => setDeferParents(false) : undefined}>
<PostThreadItem
isCollapsed={collapsedURIs.has(item.post.uri)}
isUnderCollapsed={descendantsOfCollapsedURIs.has(item.post.uri)}
onCollapse={treeView ? toggleCollapse : NO_OP}
post={item.post}
record={item.record}
threadgateRecord={threadgateRecord ?? undefined}
Expand Down
81 changes: 78 additions & 3 deletions src/view/com/post-thread/PostThreadItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ import {Text} from '#/components/Typography'
import {WhoCanReply} from '#/components/WhoCanReply'

export function PostThreadItem({
isCollapsed,
isUnderCollapsed,
onCollapse,
post,
record,
moderation,
Expand All @@ -77,6 +80,9 @@ export function PostThreadItem({
hideTopBorder,
threadgateRecord,
}: {
isCollapsed: boolean
isUnderCollapsed: boolean
onCollapse: (uri: string) => void
post: AppBskyFeedDefs.PostView
record: AppBskyFeedPost.Record
moderation: ModerationDecision | undefined
Expand Down Expand Up @@ -109,6 +115,9 @@ export function PostThreadItem({
if (richText && moderation) {
return (
<PostThreadItemLoaded
isCollapsed={isCollapsed}
isUnderCollapsed={isUnderCollapsed}
onCollapse={onCollapse}
// Safeguard from clobbering per-post state below:
key={postShadowed.uri}
post={postShadowed}
Expand Down Expand Up @@ -156,6 +165,9 @@ function PostThreadItemDeleted({hideTopBorder}: {hideTopBorder?: boolean}) {
}

let PostThreadItemLoaded = ({
isCollapsed,
isUnderCollapsed,
onCollapse,
post,
record,
richText,
Expand All @@ -174,6 +186,9 @@ let PostThreadItemLoaded = ({
hideTopBorder,
threadgateRecord,
}: {
isCollapsed: boolean
isUnderCollapsed: boolean
onCollapse: (uri: string) => void
post: Shadow<AppBskyFeedDefs.PostView>
record: AppBskyFeedPost.Record
richText: RichTextAPI
Expand Down Expand Up @@ -282,6 +297,8 @@ let PostThreadItemLoaded = ({
return <ErrorMessage message={_(msg`Invalid or unsupported post record`)} />
}

if (isUnderCollapsed) return null

if (isHighlightedPost) {
return (
<>
Expand Down Expand Up @@ -484,6 +501,47 @@ let PostThreadItemLoaded = ({
isThreadedChild && prevPost?.ctx.depth === depth && depth !== 1
const isThreadedChildAdjacentBot =
isThreadedChild && nextPost?.ctx.depth === depth

if (isCollapsed) {
return (
<PostOuterWrapper
post={post}
depth={depth}
showParentReplyLine={!!showParentReplyLine}
treeView={treeView}
hasPrecedingItem={hasPrecedingItem}
hideTopBorder={hideTopBorder}>
<PostHider
testID={`postThreadItem-by-${post.author.handle}`}
href={postHref}
onLongPress={() => {
onCollapse(post.uri)
}}
disabled={overrideBlur}
modui={moderation.ui('contentList')}
iconSize={isThreadedChild ? 24 : 42}
iconStyles={
isThreadedChild
? {marginRight: 4}
: {marginLeft: 2, marginRight: 2}
}
profile={post.author}
interpretFilterAsBlur>
<View style={[a.flex_row, a.py_xs, a.px_sm]}>
<PostMeta
author={post.author}
moderation={moderation}
timestamp={post.indexedAt}
postHref={postHref}
showHoverCard={false}
avatarSize={24}
/>
</View>
</PostHider>
</PostOuterWrapper>
)
}

return (
<PostOuterWrapper
post={post}
Expand All @@ -495,6 +553,9 @@ let PostThreadItemLoaded = ({
<PostHider
testID={`postThreadItem-by-${post.author.handle}`}
href={postHref}
onLongPress={() => {
onCollapse(post.uri)
}}
disabled={overrideBlur}
modui={moderation.ui('contentList')}
iconSize={isThreadedChild ? 24 : 42}
Expand All @@ -511,7 +572,7 @@ let PostThreadItemLoaded = ({
height: isThreadedChildAdjacentTop ? 8 : 16,
}}>
<View style={{width: 42}}>
{!isThreadedChild && showParentReplyLine && (
{showParentReplyLine && !isThreadedChild && (
<View
style={[
styles.replyLine,
Expand Down Expand Up @@ -586,6 +647,9 @@ let PostThreadItemLoaded = ({
<RichText
enableTags
value={richText}
onLinkLongPress={() => {
onCollapse(post.uri)
}}
style={[a.flex_1, a.text_md]}
numberOfLines={limitLines ? MAX_POST_LINES : undefined}
authorHandle={post.author.handle}
Expand All @@ -600,7 +664,8 @@ let PostThreadItemLoaded = ({
href="#"
/>
) : undefined}
{post.embed && (
{/* Never expand embeds for thread-view replies */}
{post.embed && shouldRenderEmbed(isThreadedChild, post.embed) && (
<View style={[a.pb_xs]}>
<PostEmbeds
embed={post.embed}
Expand Down Expand Up @@ -691,8 +756,8 @@ function PostOuterWrapper({
a.ml_sm,
t.atoms.border_contrast_low,
{
borderLeftWidth: 2,
paddingLeft: a.pl_sm.paddingLeft - 2, // minus border
borderLeftWidth: 2,
},
]}
/>
Expand Down Expand Up @@ -887,6 +952,16 @@ function getThreadAuthor(
}
}

function shouldRenderEmbed(
isThreadChild: boolean,
embed: AppBskyFeedDefs.PostView['embed'],
) {
if (isThreadChild) {
return !embed?.external
}
return true
}

const styles = StyleSheet.create({
outer: {
borderTopWidth: StyleSheet.hairlineWidth,
Expand Down
87 changes: 58 additions & 29 deletions src/view/com/util/PostMeta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface PostMetaOpts {
postHref: string
timestamp: string
showAvatar?: boolean
showHoverCard?: boolean
avatarSize?: number
onOpenAuthor?: () => void
style?: StyleProp<ViewStyle>
Expand All @@ -50,6 +51,29 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {

const timestampLabel = niceDate(i18n, opts.timestamp)

const renderedName = (
<Text
emoji
style={[
a.text_md,
opts.showHoverCard === false
? t.atoms.text_contrast_medium
: a.font_bold,
a.leading_snug,
]}>
{forceLTR(
sanitizeDisplayName(displayName, opts.moderation?.ui('displayName')),
)}
</Text>
)
const renderedHandle = (
<Text
emoji
style={[a.text_md, t.atoms.text_contrast_medium, a.leading_snug]}>
{NON_BREAKING_SPACE + sanitizeHandle(handle, '@')}
</Text>
)

return (
<View
style={[
Expand All @@ -71,36 +95,41 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
/>
</View>
)}
<ProfileHoverCard inline did={opts.author.did}>
<ProfileHoverCard
disable={opts.showHoverCard === false}
inline
did={opts.author.did}>
<Text numberOfLines={1} style={[isAndroid ? a.flex_1 : a.flex_shrink]}>
<WebOnlyInlineLinkText
to={profileLink}
label={_(msg`View profile`)}
disableMismatchWarning
onPress={onBeforePressAuthor}
style={[t.atoms.text]}>
<Text emoji style={[a.text_md, a.font_bold, a.leading_snug]}>
{forceLTR(
sanitizeDisplayName(
displayName,
opts.moderation?.ui('displayName'),
),
)}
</Text>
</WebOnlyInlineLinkText>
<WebOnlyInlineLinkText
to={profileLink}
label={_(msg`View profile`)}
disableMismatchWarning
disableUnderline
onPress={onBeforePressAuthor}
style={[a.text_md, t.atoms.text_contrast_medium, a.leading_snug]}>
<Text
emoji
style={[a.text_md, t.atoms.text_contrast_medium, a.leading_snug]}>
{NON_BREAKING_SPACE + sanitizeHandle(handle, '@')}
</Text>
</WebOnlyInlineLinkText>
{opts.showHoverCard === false ? (
<>
{renderedName}
{renderedHandle}
</>
) : (
<>
<WebOnlyInlineLinkText
to={profileLink}
label={_(msg`View profile`)}
disableMismatchWarning
onPress={onBeforePressAuthor}
style={[t.atoms.text]}>
{renderedName}
</WebOnlyInlineLinkText>
<WebOnlyInlineLinkText
to={profileLink}
label={_(msg`View profile`)}
disableMismatchWarning
disableUnderline
onPress={onBeforePressAuthor}
style={[
a.text_md,
t.atoms.text_contrast_medium,
a.leading_snug,
]}>
{renderedHandle}
</WebOnlyInlineLinkText>
</>
)}
</Text>
</ProfileHoverCard>

Expand Down