Split FeedSlice into FlatList rows (#6507)

This commit is contained in:
dan
2024-11-19 15:24:08 +00:00
committed by GitHub
parent 8d5e61e1e5
commit cfc653a5f4
3 changed files with 188 additions and 211 deletions
+116 -27
View File
@@ -42,10 +42,11 @@ import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
import {FeedErrorMessage} from './FeedErrorMessage'
import {FeedItem} from './FeedItem'
import {FeedShutdownMsg} from './FeedShutdownMsg'
import {FeedSlice} from './FeedSlice'
import {ViewFullThread} from './ViewFullThread'
type FeedItem =
type FeedRow =
| {
type: 'loading'
key: string
@@ -71,6 +72,18 @@ type FeedItem =
key: string
slice: FeedPostSlice
}
| {
type: 'sliceItem'
key: string
slice: FeedPostSlice
indexInSlice: number
showReplyTo: boolean
}
| {
type: 'sliceViewFullThread'
key: string
uri: string
}
| {
type: 'interstitialFeeds'
key: string
@@ -101,7 +114,7 @@ const followInterstitialType = 'interstitialFollows'
const progressGuideInterstitialType = 'interstitialProgressGuide'
const interstials: Record<
'following' | 'discover' | 'profile',
(FeedItem & {
(FeedRow & {
type:
| 'interstitialFeeds'
| 'interstitialFollows'
@@ -139,9 +152,9 @@ const interstials: Record<
],
}
export function getFeedPostSlice(feedItem: FeedItem): FeedPostSlice | null {
if (feedItem.type === 'slice') {
return feedItem.slice
export function getFeedPostSlice(feedRow: FeedRow): FeedPostSlice | null {
if (feedRow.type === 'sliceItem') {
return feedRow.slice
} else {
return null
}
@@ -303,8 +316,8 @@ let Feed = ({
}
}, [pollInterval])
const feedItems: FeedItem[] = React.useMemo(() => {
let arr: FeedItem[] = []
const feedItems: FeedRow[] = React.useMemo(() => {
let arr: FeedRow[] = []
if (KNOWN_SHUTDOWN_FEEDS.includes(feedUri)) {
arr.push({
type: 'feedShutdownMsg',
@@ -324,13 +337,50 @@ let Feed = ({
})
} else if (data) {
for (const page of data?.pages) {
arr = arr.concat(
page.slices.map(s => ({
type: 'slice',
slice: s,
key: s._reactKey,
})),
)
for (const slice of page.slices) {
if (slice.isIncompleteThread && slice.items.length >= 3) {
const beforeLast = slice.items.length - 2
const last = slice.items.length - 1
arr.push({
type: 'sliceItem',
key: slice.items[0]._reactKey,
slice: slice,
indexInSlice: 0,
showReplyTo: false,
})
arr.push({
type: 'sliceViewFullThread',
key: slice._reactKey + '-viewFullThread',
uri: slice.items[0].uri,
})
arr.push({
type: 'sliceItem',
key: slice.items[beforeLast]._reactKey,
slice: slice,
indexInSlice: beforeLast,
showReplyTo:
slice.items[beforeLast].parentAuthor?.did !==
slice.items[beforeLast].post.author.did,
})
arr.push({
type: 'sliceItem',
key: slice.items[last]._reactKey,
slice: slice,
indexInSlice: last,
showReplyTo: false,
})
} else {
for (let i = 0; i < slice.items.length; i++) {
arr.push({
type: 'sliceItem',
key: slice.items[i]._reactKey,
slice: slice,
indexInSlice: i,
showReplyTo: i === 0,
})
}
}
}
}
}
if (isError && !isEmpty) {
@@ -454,10 +504,10 @@ let Feed = ({
// =
const renderItem = React.useCallback(
({item, index}: ListRenderItemInfo<FeedItem>) => {
if (item.type === 'empty') {
({item: row, index: rowIndex}: ListRenderItemInfo<FeedRow>) => {
if (row.type === 'empty') {
return renderEmptyState()
} else if (item.type === 'error') {
} else if (row.type === 'error') {
return (
<FeedErrorMessage
feedDesc={feed}
@@ -466,7 +516,7 @@ let Feed = ({
savedFeedConfig={savedFeedConfig}
/>
)
} else if (item.type === 'loadMoreError') {
} else if (row.type === 'loadMoreError') {
return (
<LoadMoreRetryBtn
label={_(
@@ -475,25 +525,50 @@ let Feed = ({
onPress={onPressRetryLoadMore}
/>
)
} else if (item.type === 'loading') {
} else if (row.type === 'loading') {
return <PostFeedLoadingPlaceholder />
} else if (item.type === 'feedShutdownMsg') {
} else if (row.type === 'feedShutdownMsg') {
return <FeedShutdownMsg feedUri={feedUri} />
} else if (item.type === feedInterstitialType) {
} else if (row.type === feedInterstitialType) {
return <SuggestedFeeds />
} else if (item.type === followInterstitialType) {
} else if (row.type === followInterstitialType) {
return <SuggestedFollows feed={feed} />
} else if (item.type === progressGuideInterstitialType) {
} else if (row.type === progressGuideInterstitialType) {
return <ProgressGuide />
} else if (item.type === 'slice') {
if (item.slice.isFallbackMarker) {
} else if (row.type === 'sliceItem') {
const slice = row.slice
if (slice.isFallbackMarker) {
// HACK
// tell the user we fell back to discover
// see home.ts (feed api) for more info
// -prf
return <DiscoverFallbackHeader />
}
return <FeedSlice slice={item.slice} hideTopBorder={index === 0} />
const indexInSlice = row.indexInSlice
const item = slice.items[indexInSlice]
return (
<FeedItem
post={item.post}
record={item.record}
reason={indexInSlice === 0 ? slice.reason : undefined}
feedContext={slice.feedContext}
moderation={item.moderation}
parentAuthor={item.parentAuthor}
showReplyTo={row.showReplyTo}
isThreadParent={isThreadParentAt(slice.items, indexInSlice)}
isThreadChild={isThreadChildAt(slice.items, indexInSlice)}
isThreadLastChild={
isThreadChildAt(slice.items, indexInSlice) &&
slice.items.length === indexInSlice + 1
}
isParentBlocked={item.isParentBlocked}
isParentNotFound={item.isParentNotFound}
hideTopBorder={rowIndex === 0 && indexInSlice === 0}
rootPost={slice.items[0].post}
/>
)
} else if (row.type === 'sliceViewFullThread') {
return <ViewFullThread uri={row.uri} />
} else {
return null
}
@@ -574,3 +649,17 @@ export {Feed}
const styles = StyleSheet.create({
feedFooter: {paddingTop: 20},
})
function isThreadParentAt<T>(arr: Array<T>, i: number) {
if (arr.length === 1) {
return false
}
return i < arr.length - 1
}
function isThreadChildAt<T>(arr: Array<T>, i: number) {
if (arr.length === 1) {
return false
}
return i > 0
}
-184
View File
@@ -1,184 +0,0 @@
import React, {memo} from 'react'
import {StyleSheet, View} from 'react-native'
import Svg, {Circle, Line} from 'react-native-svg'
import {AtUri} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {usePalette} from '#/lib/hooks/usePalette'
import {makeProfileLink} from '#/lib/routes/links'
import {FeedPostSlice} from '#/state/queries/post-feed'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {SubtleWebHover} from '#/components/SubtleWebHover'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {FeedItem} from './FeedItem'
let FeedSlice = ({
slice,
hideTopBorder,
}: {
slice: FeedPostSlice
hideTopBorder?: boolean
}): React.ReactNode => {
if (slice.isIncompleteThread && slice.items.length >= 3) {
const beforeLast = slice.items.length - 2
const last = slice.items.length - 1
return (
<>
<FeedItem
key={slice.items[0]._reactKey}
post={slice.items[0].post}
record={slice.items[0].record}
reason={slice.reason}
feedContext={slice.feedContext}
parentAuthor={slice.items[0].parentAuthor}
showReplyTo={false}
moderation={slice.items[0].moderation}
isThreadParent={isThreadParentAt(slice.items, 0)}
isThreadChild={isThreadChildAt(slice.items, 0)}
hideTopBorder={hideTopBorder}
isParentBlocked={slice.items[0].isParentBlocked}
isParentNotFound={slice.items[0].isParentNotFound}
rootPost={slice.items[0].post}
/>
<ViewFullThread uri={slice.items[0].uri} />
<FeedItem
key={slice.items[beforeLast]._reactKey}
post={slice.items[beforeLast].post}
record={slice.items[beforeLast].record}
reason={undefined}
feedContext={slice.feedContext}
parentAuthor={slice.items[beforeLast].parentAuthor}
showReplyTo={
slice.items[beforeLast].parentAuthor?.did !==
slice.items[beforeLast].post.author.did
}
moderation={slice.items[beforeLast].moderation}
isThreadParent={isThreadParentAt(slice.items, beforeLast)}
isThreadChild={isThreadChildAt(slice.items, beforeLast)}
isParentBlocked={slice.items[beforeLast].isParentBlocked}
isParentNotFound={slice.items[beforeLast].isParentNotFound}
rootPost={slice.items[0].post}
/>
<FeedItem
key={slice.items[last]._reactKey}
post={slice.items[last].post}
record={slice.items[last].record}
reason={undefined}
feedContext={slice.feedContext}
parentAuthor={slice.items[last].parentAuthor}
showReplyTo={false}
moderation={slice.items[last].moderation}
isThreadParent={isThreadParentAt(slice.items, last)}
isThreadChild={isThreadChildAt(slice.items, last)}
isParentBlocked={slice.items[last].isParentBlocked}
isParentNotFound={slice.items[last].isParentNotFound}
isThreadLastChild
rootPost={slice.items[0].post}
/>
</>
)
}
return (
<>
{slice.items.map((item, i) => (
<FeedItem
key={item._reactKey}
post={slice.items[i].post}
record={slice.items[i].record}
reason={i === 0 ? slice.reason : undefined}
feedContext={slice.feedContext}
moderation={slice.items[i].moderation}
parentAuthor={slice.items[i].parentAuthor}
showReplyTo={i === 0}
isThreadParent={isThreadParentAt(slice.items, i)}
isThreadChild={isThreadChildAt(slice.items, i)}
isThreadLastChild={
isThreadChildAt(slice.items, i) && slice.items.length === i + 1
}
isParentBlocked={slice.items[i].isParentBlocked}
isParentNotFound={slice.items[i].isParentNotFound}
hideTopBorder={hideTopBorder && i === 0}
rootPost={slice.items[0].post}
/>
))}
</>
)
}
FeedSlice = memo(FeedSlice)
export {FeedSlice}
function ViewFullThread({uri}: {uri: string}) {
const {
state: hover,
onIn: onHoverIn,
onOut: onHoverOut,
} = useInteractionState()
const pal = usePalette('default')
const itemHref = React.useMemo(() => {
const urip = new AtUri(uri)
return makeProfileLink({did: urip.hostname, handle: ''}, 'post', urip.rkey)
}, [uri])
return (
<Link
style={[styles.viewFullThread]}
href={itemHref}
asAnchor
noFeedback
onPointerEnter={onHoverIn}
onPointerLeave={onHoverOut}>
<SubtleWebHover
hover={hover}
// adjust position for visual alignment - the actual box has lots of top padding and not much bottom padding -sfn
style={{top: 8, bottom: -5}}
/>
<View style={styles.viewFullThreadDots}>
<Svg width="4" height="40">
<Line
x1="2"
y1="0"
x2="2"
y2="15"
stroke={pal.colors.replyLine}
strokeWidth="2"
/>
<Circle cx="2" cy="22" r="1.5" fill={pal.colors.replyLineDot} />
<Circle cx="2" cy="28" r="1.5" fill={pal.colors.replyLineDot} />
<Circle cx="2" cy="34" r="1.5" fill={pal.colors.replyLineDot} />
</Svg>
</View>
<Text type="md" style={[pal.link, {paddingTop: 18, paddingBottom: 4}]}>
<Trans>View full thread</Trans>
</Text>
</Link>
)
}
const styles = StyleSheet.create({
viewFullThread: {
flexDirection: 'row',
gap: 10,
paddingLeft: 18,
},
viewFullThreadDots: {
width: 42,
alignItems: 'center',
},
})
function isThreadParentAt<T>(arr: Array<T>, i: number) {
if (arr.length === 1) {
return false
}
return i < arr.length - 1
}
function isThreadChildAt<T>(arr: Array<T>, i: number) {
if (arr.length === 1) {
return false
}
return i > 0
}
+72
View File
@@ -0,0 +1,72 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import Svg, {Circle, Line} from 'react-native-svg'
import {AtUri} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {usePalette} from '#/lib/hooks/usePalette'
import {makeProfileLink} from '#/lib/routes/links'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {SubtleWebHover} from '#/components/SubtleWebHover'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
export function ViewFullThread({uri}: {uri: string}) {
const {
state: hover,
onIn: onHoverIn,
onOut: onHoverOut,
} = useInteractionState()
const pal = usePalette('default')
const itemHref = React.useMemo(() => {
const urip = new AtUri(uri)
return makeProfileLink({did: urip.hostname, handle: ''}, 'post', urip.rkey)
}, [uri])
return (
<Link
style={[styles.viewFullThread]}
href={itemHref}
asAnchor
noFeedback
onPointerEnter={onHoverIn}
onPointerLeave={onHoverOut}>
<SubtleWebHover
hover={hover}
// adjust position for visual alignment - the actual box has lots of top padding and not much bottom padding -sfn
style={{top: 8, bottom: -5}}
/>
<View style={styles.viewFullThreadDots}>
<Svg width="4" height="40">
<Line
x1="2"
y1="0"
x2="2"
y2="15"
stroke={pal.colors.replyLine}
strokeWidth="2"
/>
<Circle cx="2" cy="22" r="1.5" fill={pal.colors.replyLineDot} />
<Circle cx="2" cy="28" r="1.5" fill={pal.colors.replyLineDot} />
<Circle cx="2" cy="34" r="1.5" fill={pal.colors.replyLineDot} />
</Svg>
</View>
<Text type="md" style={[pal.link, {paddingTop: 18, paddingBottom: 4}]}>
<Trans>View full thread</Trans>
</Text>
</Link>
)
}
const styles = StyleSheet.create({
viewFullThread: {
flexDirection: 'row',
gap: 10,
paddingLeft: 18,
},
viewFullThreadDots: {
width: 42,
alignItems: 'center',
},
})