Split FeedSlice into FlatList rows (#6507)
This commit is contained in:
+116
-27
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user