diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/view/com/posts/Feed.tsx | 143 | ||||
-rw-r--r-- | src/view/com/posts/FeedSlice.tsx | 184 | ||||
-rw-r--r-- | src/view/com/posts/ViewFullThread.tsx | 72 |
3 files changed, 188 insertions, 211 deletions
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index d6cf6dac5..07c1fd6b7 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -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 @@ -72,6 +73,18 @@ type FeedItem = slice: FeedPostSlice } | { + type: 'sliceItem' + key: string + slice: FeedPostSlice + indexInSlice: number + showReplyTo: boolean + } + | { + type: 'sliceViewFullThread' + key: string + uri: string + } + | { type: 'interstitialFeeds' key: string params: { @@ -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 +} diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx deleted file mode 100644 index 09335fa0e..000000000 --- a/src/view/com/posts/FeedSlice.tsx +++ /dev/null @@ -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 -} diff --git a/src/view/com/posts/ViewFullThread.tsx b/src/view/com/posts/ViewFullThread.tsx new file mode 100644 index 000000000..0b347f22c --- /dev/null +++ b/src/view/com/posts/ViewFullThread.tsx @@ -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', + }, +}) |