diff options
Diffstat (limited to 'src/view/com/post-thread')
-rw-r--r-- | src/view/com/post-thread/PostThread.tsx | 90 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 483 |
2 files changed, 349 insertions, 224 deletions
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 51f63dbb3..399e47006 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -20,25 +20,37 @@ import {ComposePrompt} from '../composer/Prompt' import {ErrorMessage} from '../util/error/ErrorMessage' import {Text} from '../util/text/Text' import {s} from 'lib/styles' -import {isDesktopWeb, isMobileWeb} from 'platform/detection' +import {isIOS, isDesktopWeb, isMobileWeb} from 'platform/detection' import {usePalette} from 'lib/hooks/usePalette' import {useSetTitle} from 'lib/hooks/useSetTitle' import {useNavigation} from '@react-navigation/native' import {NavigationProp} from 'lib/routes/types' import {sanitizeDisplayName} from 'lib/strings/display-names' +const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 0} + +const PARENT_SPINNER = { + _reactKey: '__parent_spinner__', + _isHighlightedPost: false, +} const REPLY_PROMPT = {_reactKey: '__reply__', _isHighlightedPost: false} const DELETED = {_reactKey: '__deleted__', _isHighlightedPost: false} const BLOCKED = {_reactKey: '__blocked__', _isHighlightedPost: false} +const CHILD_SPINNER = { + _reactKey: '__child_spinner__', + _isHighlightedPost: false, +} const BOTTOM_COMPONENT = { _reactKey: '__bottom_component__', _isHighlightedPost: false, } type YieldedItem = | PostThreadItemModel + | typeof PARENT_SPINNER | typeof REPLY_PROMPT | typeof DELETED | typeof BLOCKED + | typeof PARENT_SPINNER export const PostThread = observer(function PostThread({ uri, @@ -51,14 +63,24 @@ export const PostThread = observer(function PostThread({ }) { const pal = usePalette('default') const ref = useRef<FlatList>(null) + const hasScrolledIntoView = useRef<boolean>(false) const [isRefreshing, setIsRefreshing] = React.useState(false) const navigation = useNavigation<NavigationProp>() const posts = React.useMemo(() => { if (view.thread) { - return Array.from(flattenThread(view.thread)).concat([BOTTOM_COMPONENT]) + const arr = Array.from(flattenThread(view.thread)) + if (view.isLoadingFromCache) { + if (view.thread?.postRecord?.reply) { + arr.unshift(PARENT_SPINNER) + } + arr.push(CHILD_SPINNER) + } else { + arr.push(BOTTOM_COMPONENT) + } + return arr } return [] - }, [view.thread]) + }, [view.isLoadingFromCache, view.thread]) useSetTitle( view.thread?.postRecord && `${sanitizeDisplayName( @@ -80,17 +102,37 @@ export const PostThread = observer(function PostThread({ setIsRefreshing(false) }, [view, setIsRefreshing]) - const onLayout = React.useCallback(() => { + const onContentSizeChange = React.useCallback(() => { + // only run once + if (hasScrolledIntoView.current) { + return + } + + // wait for loading to finish + if ( + !view.hasContent || + (view.isFromCache && view.isLoadingFromCache) || + view.isLoading + ) { + return + } + const index = posts.findIndex(post => post._isHighlightedPost) if (index !== -1) { ref.current?.scrollToIndex({ index, animated: false, - viewOffset: 40, + viewPosition: 0, }) + hasScrolledIntoView.current = true } - }, [posts, ref]) - + }, [ + posts, + view.hasContent, + view.isFromCache, + view.isLoadingFromCache, + view.isLoading, + ]) const onScrollToIndexFailed = React.useCallback( (info: { index: number @@ -115,7 +157,13 @@ export const PostThread = observer(function PostThread({ const renderItem = React.useCallback( ({item}: {item: YieldedItem}) => { - if (item === REPLY_PROMPT) { + if (item === PARENT_SPINNER) { + return ( + <View style={styles.parentSpinner}> + <ActivityIndicator /> + </View> + ) + } else if (item === REPLY_PROMPT) { return <ComposePrompt onPressCompose={onPressReply} /> } else if (item === DELETED) { return ( @@ -150,6 +198,12 @@ export const PostThread = observer(function PostThread({ ]} /> ) + } else if (item === CHILD_SPINNER) { + return ( + <View style={styles.childSpinner}> + <ActivityIndicator /> + </View> + ) } else if (item instanceof PostThreadItemModel) { return <PostThreadItem item={item} onPostReply={onRefresh} /> } @@ -247,6 +301,11 @@ export const PostThread = observer(function PostThread({ ref={ref} data={posts} initialNumToRender={posts.length} + maintainVisibleContentPosition={ + isIOS && view.isFromCache + ? MAINTAIN_VISIBLE_CONTENT_POSITION + : undefined + } keyExtractor={item => item._reactKey} renderItem={renderItem} refreshControl={ @@ -257,10 +316,12 @@ export const PostThread = observer(function PostThread({ titleColor={pal.colors.text} /> } - onLayout={onLayout} + onContentSizeChange={ + isIOS && view.isFromCache ? undefined : onContentSizeChange + } onScrollToIndexFailed={onScrollToIndexFailed} style={s.hContentRegion} - contentContainerStyle={s.contentContainerExtra} + contentContainerStyle={styles.contentContainerExtra} /> ) }) @@ -307,10 +368,17 @@ const styles = StyleSheet.create({ paddingHorizontal: 18, paddingVertical: 18, }, + parentSpinner: { + paddingVertical: 10, + }, + childSpinner: {}, bottomBorder: { borderBottomWidth: 1, }, bottomSpacer: { - height: 200, + height: 400, + }, + contentContainerExtra: { + paddingBottom: 500, }, }) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index edf8d7749..8a56012f0 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -26,15 +26,14 @@ import {PostEmbeds} from '../util/post-embeds' import {PostCtrls} from '../util/post-ctrls/PostCtrls' import {PostHider} from '../util/moderation/PostHider' import {ContentHider} from '../util/moderation/ContentHider' -import {ImageHider} from '../util/moderation/ImageHider' +import {PostAlerts} from '../util/moderation/PostAlerts' import {PostSandboxWarning} from '../util/PostSandboxWarning' import {ErrorMessage} from '../util/error/ErrorMessage' import {usePalette} from 'lib/hooks/usePalette' import {formatCount} from '../util/numeric/format' import {TimeElapsed} from 'view/com/util/TimeElapsed' import {makeProfileLink} from 'lib/routes/links' - -const PARENT_REPLY_LINE_LENGTH = 8 +import {isDesktopWeb} from 'platform/detection' export const PostThreadItem = observer(function PostThreadItem({ item, @@ -69,8 +68,7 @@ export const PostThreadItem = observer(function PostThreadItem({ }, [item.post.uri, item.post.author]) const repostsTitle = 'Reposts of this post' - const primaryLanguage = store.preferences.contentLanguages[0] || 'en' - const translatorUrl = getTranslatorLink(primaryLanguage, record?.text || '') + const translatorUrl = getTranslatorLink(record?.text || '') const needsTranslation = useMemo( () => store.preferences.contentLanguages.length > 0 && @@ -159,159 +157,197 @@ export const PostThreadItem = observer(function PostThreadItem({ if (item._isHighlightedPost) { return ( - <PostHider - testID={`postThreadItem-by-${item.post.author.handle}`} - style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]} - moderation={item.moderation.thread}> - <PostSandboxWarning /> - <View style={styles.layout}> - <View style={styles.layoutAvi}> - <PreviewableUserAvatar - size={52} - did={item.post.author.did} - handle={item.post.author.handle} - avatar={item.post.author.avatar} - moderation={item.moderation.avatar} - /> + <> + {item.rootUri !== item.uri && ( + <View style={{paddingLeft: 18, flexDirection: 'row', height: 16}}> + <View style={{width: 52}}> + <View + style={[ + styles.replyLine, + { + flexGrow: 1, + backgroundColor: pal.colors.replyLine, + }, + ]} + /> + </View> </View> - <View style={styles.layoutContent}> - <View style={[styles.meta, styles.metaExpandedLine1]}> - <View style={[s.flexRow]}> + )} + + <Link + testID={`postThreadItem-by-${item.post.author.handle}`} + style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]} + noFeedback + accessible={false}> + <PostSandboxWarning /> + <View style={styles.layout}> + <View style={[styles.layoutAvi, {paddingBottom: 8}]}> + <PreviewableUserAvatar + size={52} + did={item.post.author.did} + handle={item.post.author.handle} + avatar={item.post.author.avatar} + moderation={item.moderation.avatar} + /> + </View> + <View style={styles.layoutContent}> + <View style={[styles.meta, styles.metaExpandedLine1]}> + <View style={[s.flexRow]}> + <Link + style={styles.metaItem} + href={authorHref} + title={authorTitle}> + <Text + type="xl-bold" + style={[pal.text]} + numberOfLines={1} + lineHeight={1.2}> + {sanitizeDisplayName( + item.post.author.displayName || + sanitizeHandle(item.post.author.handle), + )} + </Text> + </Link> + <Text type="md" style={[styles.metaItem, pal.textLight]}> + · + <TimeElapsed timestamp={item.post.indexedAt}> + {({timeElapsed}) => <>{timeElapsed}</>} + </TimeElapsed> + </Text> + </View> + </View> + <View style={styles.meta}> <Link style={styles.metaItem} href={authorHref} title={authorTitle}> - <Text - type="xl-bold" - style={[pal.text]} - numberOfLines={1} - lineHeight={1.2}> - {sanitizeDisplayName( - item.post.author.displayName || - sanitizeHandle(item.post.author.handle), - )} + <Text type="md" style={[pal.textLight]} numberOfLines={1}> + {sanitizeHandle(item.post.author.handle, '@')} </Text> </Link> - <Text type="md" style={[styles.metaItem, pal.textLight]}> - · - <TimeElapsed timestamp={item.post.indexedAt}> - {({timeElapsed}) => <>{timeElapsed}</>} - </TimeElapsed> - </Text> - </View> - <View style={s.flex1} /> - <PostDropdownBtn - testID="postDropdownBtn" - itemUri={itemUri} - itemCid={itemCid} - itemHref={itemHref} - itemTitle={itemTitle} - isAuthor={item.post.author.did === store.me.did} - isThreadMuted={item.isThreadMuted} - onCopyPostText={onCopyPostText} - onOpenTranslate={onOpenTranslate} - onToggleThreadMute={onToggleThreadMute} - onDeletePost={onDeletePost} - /> - </View> - <View style={styles.meta}> - <Link - style={styles.metaItem} - href={authorHref} - title={authorTitle}> - <Text type="md" style={[pal.textLight]} numberOfLines={1}> - {sanitizeHandle(item.post.author.handle, '@')} - </Text> - </Link> - </View> - </View> - </View> - <View style={[s.pl10, s.pr10, s.pb10]}> - <ContentHider moderation={item.moderation.view}> - {item.richText?.text ? ( - <View - style={[ - styles.postTextContainer, - styles.postTextLargeContainer, - ]}> - <RichText - type="post-text-lg" - richText={item.richText} - lineHeight={1.3} - style={s.flex1} - /> </View> - ) : undefined} - <ImageHider moderation={item.moderation.view} style={s.mb10}> - <PostEmbeds embed={item.post.embed} style={s.mb10} /> - </ImageHider> - </ContentHider> - <ExpandedPostDetails - post={item.post} - translatorUrl={translatorUrl} - needsTranslation={needsTranslation} - /> - {hasEngagement ? ( - <View style={[styles.expandedInfo, pal.border]}> - {item.post.repostCount ? ( - <Link - style={styles.expandedInfoItem} - href={repostsHref} - title={repostsTitle}> - <Text testID="repostCount" type="lg" style={pal.textLight}> - <Text type="xl-bold" style={pal.text}> - {formatCount(item.post.repostCount)} - </Text>{' '} - {pluralize(item.post.repostCount, 'repost')} - </Text> - </Link> - ) : ( - <></> - )} - {item.post.likeCount ? ( - <Link - style={styles.expandedInfoItem} - href={likesHref} - title={likesTitle}> - <Text testID="likeCount" type="lg" style={pal.textLight}> - <Text type="xl-bold" style={pal.text}> - {formatCount(item.post.likeCount)} - </Text>{' '} - {pluralize(item.post.likeCount, 'like')} - </Text> - </Link> - ) : ( - <></> - )} </View> - ) : ( - <></> - )} - <View style={[s.pl10, s.pb5]}> - <PostCtrls - big + <PostDropdownBtn + testID="postDropdownBtn" itemUri={itemUri} itemCid={itemCid} itemHref={itemHref} itemTitle={itemTitle} - author={item.post.author} - text={item.richText?.text || record.text} - indexedAt={item.post.indexedAt} isAuthor={item.post.author.did === store.me.did} - isReposted={!!item.post.viewer?.repost} - isLiked={!!item.post.viewer?.like} isThreadMuted={item.isThreadMuted} - onPressReply={onPressReply} - onPressToggleRepost={onPressToggleRepost} - onPressToggleLike={onPressToggleLike} onCopyPostText={onCopyPostText} onOpenTranslate={onOpenTranslate} onToggleThreadMute={onToggleThreadMute} onDeletePost={onDeletePost} + style={{ + paddingVertical: 6, + paddingHorizontal: 10, + marginLeft: 'auto', + width: 40, + }} + /> + </View> + <View style={[s.pl10, s.pr10, s.pb10]}> + <ContentHider + moderation={item.moderation.content} + ignoreMute + style={styles.contentHider} + childContainerStyle={styles.contentHiderChild}> + <PostAlerts + moderation={item.moderation.content} + includeMute + style={styles.alert} + /> + {item.richText?.text ? ( + <View + style={[ + styles.postTextContainer, + styles.postTextLargeContainer, + ]}> + <RichText + type="post-text-lg" + richText={item.richText} + lineHeight={1.3} + style={s.flex1} + /> + </View> + ) : undefined} + {item.post.embed && ( + <ContentHider moderation={item.moderation.embed} style={s.mb10}> + <PostEmbeds + embed={item.post.embed} + moderation={item.moderation.embed} + /> + </ContentHider> + )} + </ContentHider> + <ExpandedPostDetails + post={item.post} + translatorUrl={translatorUrl} + needsTranslation={needsTranslation} /> + {hasEngagement ? ( + <View style={[styles.expandedInfo, pal.border]}> + {item.post.repostCount ? ( + <Link + style={styles.expandedInfoItem} + href={repostsHref} + title={repostsTitle}> + <Text testID="repostCount" type="lg" style={pal.textLight}> + <Text type="xl-bold" style={pal.text}> + {formatCount(item.post.repostCount)} + </Text>{' '} + {pluralize(item.post.repostCount, 'repost')} + </Text> + </Link> + ) : ( + <></> + )} + {item.post.likeCount ? ( + <Link + style={styles.expandedInfoItem} + href={likesHref} + title={likesTitle}> + <Text testID="likeCount" type="lg" style={pal.textLight}> + <Text type="xl-bold" style={pal.text}> + {formatCount(item.post.likeCount)} + </Text>{' '} + {pluralize(item.post.likeCount, 'like')} + </Text> + </Link> + ) : ( + <></> + )} + </View> + ) : ( + <></> + )} + <View style={[s.pl10, s.pb5]}> + <PostCtrls + big + itemUri={itemUri} + itemCid={itemCid} + itemHref={itemHref} + itemTitle={itemTitle} + author={item.post.author} + text={item.richText?.text || record.text} + indexedAt={item.post.indexedAt} + isAuthor={item.post.author.did === store.me.did} + isReposted={!!item.post.viewer?.repost} + isLiked={!!item.post.viewer?.like} + isThreadMuted={item.isThreadMuted} + onPressReply={onPressReply} + onPressToggleRepost={onPressToggleRepost} + onPressToggleLike={onPressToggleLike} + onCopyPostText={onCopyPostText} + onOpenTranslate={onOpenTranslate} + onToggleThreadMute={onToggleThreadMute} + onDeletePost={onDeletePost} + /> + </View> </View> - </View> - </PostHider> + </Link> + </> ) } else { return ( @@ -324,26 +360,36 @@ export const PostThreadItem = observer(function PostThreadItem({ pal.border, pal.view, item._showParentReplyLine && styles.noTopBorder, + !item._showChildReplyLine && {borderBottomWidth: 1}, ]} - moderation={item.moderation.thread}> - {item._showParentReplyLine && ( - <View - style={[ - styles.parentReplyLine, - {borderColor: pal.colors.replyLine}, - ]} - /> - )} - {item._showChildReplyLine && ( - <View - style={[ - styles.childReplyLine, - {borderColor: pal.colors.replyLine}, - ]} - /> - )} + moderation={item.moderation.content}> <PostSandboxWarning /> - <View style={styles.layout}> + + <View + style={{flexDirection: 'row', gap: 10, paddingLeft: 8, height: 16}}> + <View style={{width: 52}}> + {item._showParentReplyLine && ( + <View + style={[ + styles.replyLine, + { + flexGrow: 1, + backgroundColor: pal.colors.replyLine, + marginBottom: 4, + }, + ]} + /> + )} + </View> + </View> + + <View + style={[ + styles.layout, + { + paddingBottom: item._showChildReplyLine ? 0 : 8, + }, + ]}> <View style={styles.layoutAvi}> <PreviewableUserAvatar size={52} @@ -352,7 +398,21 @@ export const PostThreadItem = observer(function PostThreadItem({ avatar={item.post.author.avatar} moderation={item.moderation.avatar} /> + + {item._showChildReplyLine && ( + <View + style={[ + styles.replyLine, + { + flexGrow: 1, + backgroundColor: pal.colors.replyLine, + marginTop: 4, + }, + ]} + /> + )} </View> + <View style={styles.layoutContent}> <PostMeta author={item.post.author} @@ -360,32 +420,39 @@ export const PostThreadItem = observer(function PostThreadItem({ timestamp={item.post.indexedAt} postHref={itemHref} /> - <ContentHider - moderation={item.moderation.thread} - containerStyle={styles.contentHider}> - {item.richText?.text ? ( - <View style={styles.postTextContainer}> - <RichText - type="post-text" - richText={item.richText} - style={[pal.text, s.flex1]} - lineHeight={1.3} - /> - </View> - ) : undefined} - <ImageHider style={s.mb10} moderation={item.moderation.thread}> - <PostEmbeds embed={item.post.embed} style={s.mb10} /> - </ImageHider> - {needsTranslation && ( - <View style={[pal.borderDark, styles.translateLink]}> - <Link href={translatorUrl} title="Translate"> - <Text type="sm" style={pal.link}> - Translate this post - </Text> - </Link> - </View> - )} - </ContentHider> + <PostAlerts + moderation={item.moderation.content} + style={styles.alert} + /> + {item.richText?.text ? ( + <View style={styles.postTextContainer}> + <RichText + type="post-text" + richText={item.richText} + style={[pal.text, s.flex1]} + lineHeight={1.3} + /> + </View> + ) : undefined} + {item.post.embed && ( + <ContentHider + style={styles.contentHider} + moderation={item.moderation.embed}> + <PostEmbeds + embed={item.post.embed} + moderation={item.moderation.embed} + /> + </ContentHider> + )} + {needsTranslation && ( + <View style={[pal.borderDark, styles.translateLink]}> + <Link href={translatorUrl} title="Translate"> + <Text type="sm" style={pal.link}> + Translate this post + </Text> + </Link> + </View> + )} <PostCtrls itemUri={itemUri} itemCid={itemCid} @@ -416,7 +483,7 @@ export const PostThreadItem = observer(function PostThreadItem({ <Link style={[ styles.loadMore, - {borderTopColor: pal.colors.border}, + {borderBottomColor: pal.colors.border}, pal.view, ]} href={itemHref} @@ -466,41 +533,22 @@ const styles = StyleSheet.create({ paddingLeft: 10, }, outerHighlighted: { - paddingTop: 2, - paddingLeft: 6, - paddingRight: 6, + paddingTop: 16, + paddingLeft: 10, + paddingRight: 10, }, noTopBorder: { borderTopWidth: 0, }, - parentReplyLine: { - position: 'absolute', - left: 44, - top: -1 * PARENT_REPLY_LINE_LENGTH + 6, - height: PARENT_REPLY_LINE_LENGTH, - borderLeftWidth: 2, - }, - childReplyLine: { - position: 'absolute', - left: 44, - top: 65, - bottom: 0, - borderLeftWidth: 2, - }, layout: { flexDirection: 'row', + gap: 10, + paddingLeft: 8, }, - layoutAvi: { - paddingLeft: 10, - paddingTop: 10, - paddingBottom: 10, - marginRight: 10, - }, + layoutAvi: {}, layoutContent: { flex: 1, paddingRight: 10, - paddingTop: 10, - paddingBottom: 10, }, meta: { flexDirection: 'row', @@ -513,7 +561,10 @@ const styles = StyleSheet.create({ }, metaItem: { paddingRight: 5, - maxWidth: 240, + maxWidth: isDesktopWeb ? 380 : 220, + }, + alert: { + marginBottom: 6, }, postTextContainer: { flexDirection: 'row', @@ -521,7 +572,6 @@ const styles = StyleSheet.create({ flexWrap: 'wrap', paddingBottom: 8, paddingRight: 10, - minHeight: 36, }, postTextLargeContainer: { paddingHorizontal: 0, @@ -531,7 +581,10 @@ const styles = StyleSheet.create({ marginBottom: 6, }, contentHider: { - marginTop: 4, + marginBottom: 6, + }, + contentHiderChild: { + marginTop: 6, }, expandedInfo: { flexDirection: 'row', @@ -547,10 +600,14 @@ const styles = StyleSheet.create({ loadMore: { flexDirection: 'row', justifyContent: 'space-between', - borderTopWidth: 1, + borderBottomWidth: 1, paddingLeft: 80, paddingRight: 20, - paddingVertical: 10, - marginBottom: 8, + paddingVertical: 12, + }, + replyLine: { + width: 2, + marginLeft: 'auto', + marginRight: 'auto', }, }) |