diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/state/models/ui/preferences.ts | 15 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThread.tsx | 48 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 141 | ||||
-rw-r--r-- | src/view/index.ts | 2 | ||||
-rw-r--r-- | src/view/screens/PostThread.tsx | 1 | ||||
-rw-r--r-- | src/view/screens/PreferencesHomeFeed.tsx | 6 | ||||
-rw-r--r-- | src/view/screens/PreferencesThreads.tsx | 18 |
7 files changed, 178 insertions, 53 deletions
diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index 03f08bc1b..5c6ea230b 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -58,6 +58,7 @@ export class PreferencesModel { homeFeedMergeFeedEnabled: boolean = false threadDefaultSort: string = 'oldest' threadFollowedUsersFirst: boolean = true + threadTreeViewEnabled: boolean = false requireAltTextEnabled: boolean = false // used to linearize async modifications to state @@ -91,6 +92,7 @@ export class PreferencesModel { homeFeedMergeFeedEnabled: this.homeFeedMergeFeedEnabled, threadDefaultSort: this.threadDefaultSort, threadFollowedUsersFirst: this.threadFollowedUsersFirst, + threadTreeViewEnabled: this.threadTreeViewEnabled, requireAltTextEnabled: this.requireAltTextEnabled, } } @@ -202,13 +204,20 @@ export class PreferencesModel { ) { this.threadDefaultSort = v.threadDefaultSort } - // check if tread followed-users-first is enabled in preferences, then hydrate + // check if thread followed-users-first is enabled in preferences, then hydrate if ( hasProp(v, 'threadFollowedUsersFirst') && typeof v.threadFollowedUsersFirst === 'boolean' ) { this.threadFollowedUsersFirst = v.threadFollowedUsersFirst } + // check if thread treeview is enabled in preferences, then hydrate + if ( + hasProp(v, 'threadTreeViewEnabled') && + typeof v.threadTreeViewEnabled === 'boolean' + ) { + this.threadTreeViewEnabled = v.threadTreeViewEnabled + } // check if requiring alt text is enabled in preferences, then hydrate if ( hasProp(v, 'requireAltTextEnabled') && @@ -524,6 +533,10 @@ export class PreferencesModel { this.threadFollowedUsersFirst = !this.threadFollowedUsersFirst } + toggleThreadTreeViewEnabled() { + this.threadTreeViewEnabled = !this.threadTreeViewEnabled + } + toggleRequireAltTextEnabled() { this.requireAltTextEnabled = !this.requireAltTextEnabled } diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 1cc177d17..373b4499d 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -55,6 +55,7 @@ const LOAD_MORE = { const BOTTOM_COMPONENT = { _reactKey: '__bottom_component__', _isHighlightedPost: false, + _showBorder: true, } type YieldedItem = | PostThreadItemModel @@ -69,10 +70,12 @@ export const PostThread = observer(function PostThread({ uri, view, onPressReply, + treeView, }: { uri: string view: PostThreadModel onPressReply: () => void + treeView: boolean }) { const pal = usePalette('default') const {isTablet} = useWebMediaQueries() @@ -99,6 +102,13 @@ export const PostThread = observer(function PostThread({ } return [] }, [view.isLoadingFromCache, view.thread, maxVisible]) + const highlightedPostIndex = posts.findIndex(post => post._isHighlightedPost) + const showBottomBorder = + !treeView || + // in the treeview, only show the bottom border + // if there are replies under the highlighted posts + posts.findLast(v => v instanceof PostThreadItemModel) !== + posts[highlightedPostIndex] useSetTitle( view.thread?.postRecord && `${sanitizeDisplayName( @@ -135,17 +145,16 @@ export const PostThread = observer(function PostThread({ return } - const index = posts.findIndex(post => post._isHighlightedPost) - if (index !== -1) { + if (highlightedPostIndex !== -1) { ref.current?.scrollToIndex({ - index, + index: highlightedPostIndex, animated: false, viewPosition: 0, }) hasScrolledIntoView.current = true } }, [ - posts, + highlightedPostIndex, view.hasContent, view.isFromCache, view.isLoadingFromCache, @@ -184,7 +193,14 @@ export const PostThread = observer(function PostThread({ </View> ) } else if (item === REPLY_PROMPT) { - return <ComposePrompt onPressCompose={onPressReply} /> + return ( + <View + style={ + treeView && [pal.border, {borderBottomWidth: 1, marginBottom: 6}] + }> + {isDesktopWeb && <ComposePrompt onPressCompose={onPressReply} />} + </View> + ) } else if (item === DELETED) { return ( <View style={[pal.border, pal.viewLight, styles.itemContainer]}> @@ -224,7 +240,18 @@ export const PostThread = observer(function PostThread({ // due to some complexities with how flatlist works, this is the easiest way // I could find to get a border positioned directly under the last item // -prf - return <View style={[pal.border, styles.bottomSpacer]} /> + return ( + <View + style={[ + {height: 400}, + showBottomBorder && { + borderTopWidth: 1, + borderColor: pal.colors.border, + }, + treeView && {marginTop: 10}, + ]} + /> + ) } else if (item === CHILD_SPINNER) { return ( <View style={styles.childSpinner}> @@ -240,12 +267,13 @@ export const PostThread = observer(function PostThread({ item={item} onPostReply={onRefresh} hasPrecedingItem={prev?._showChildReplyLine} + treeView={treeView} /> ) } return <></> }, - [onRefresh, onPressReply, pal, posts, isTablet], + [onRefresh, onPressReply, pal, posts, isTablet, treeView, showBottomBorder], ) // loading @@ -377,7 +405,7 @@ function* flattenThread( } } yield post - if (isDesktopWeb && post._isHighlightedPost) { + if (post._isHighlightedPost) { yield REPLY_PROMPT } if (post.replies?.length) { @@ -411,8 +439,4 @@ const styles = StyleSheet.create({ paddingVertical: 10, }, childSpinner: {}, - bottomSpacer: { - height: 400, - borderTopWidth: 1, - }, }) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 37c7ece47..1089bfabf 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -35,15 +35,18 @@ import {formatCount} from '../util/numeric/format' import {TimeElapsed} from 'view/com/util/TimeElapsed' import {makeProfileLink} from 'lib/routes/links' import {isDesktopWeb} from 'platform/detection' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' export const PostThreadItem = observer(function PostThreadItem({ item, onPostReply, hasPrecedingItem, + treeView, }: { item: PostThreadItemModel onPostReply: () => void hasPrecedingItem: boolean + treeView: boolean }) { const pal = usePalette('default') const store = useStores() @@ -389,25 +392,28 @@ export const PostThreadItem = observer(function PostThreadItem({ </> ) } else { + const isThreadedChild = treeView && item._depth > 0 return ( - <> + <PostOuterWrapper + item={item} + hasPrecedingItem={hasPrecedingItem} + treeView={treeView}> <PostHider testID={`postThreadItem-by-${item.post.author.handle}`} href={itemHref} - style={[ - styles.outer, - pal.border, - pal.view, - item._showParentReplyLine && hasPrecedingItem && styles.noTopBorder, - styles.cursor, - ]} + style={[pal.view]} moderation={item.moderation.content}> <PostSandboxWarning /> <View - style={{flexDirection: 'row', gap: 10, paddingLeft: 8, height: 16}}> + style={{ + flexDirection: 'row', + gap: 10, + paddingLeft: 8, + height: isThreadedChild ? 8 : 16, + }}> <View style={{width: 52}}> - {item._showParentReplyLine && ( + {!isThreadedChild && item._showParentReplyLine && ( <View style={[ styles.replyLine, @@ -431,7 +437,7 @@ export const PostThreadItem = observer(function PostThreadItem({ ]}> <View style={styles.layoutAvi}> <PreviewableUserAvatar - size={52} + size={isThreadedChild ? 24 : 52} did={item.post.author.did} handle={item.post.author.handle} avatar={item.post.author.avatar} @@ -444,7 +450,9 @@ export const PostThreadItem = observer(function PostThreadItem({ styles.replyLine, { flexGrow: 1, - backgroundColor: pal.colors.replyLine, + backgroundColor: isThreadedChild + ? pal.colors.border + : pal.colors.replyLine, marginTop: 4, }, ]} @@ -464,7 +472,11 @@ export const PostThreadItem = observer(function PostThreadItem({ style={styles.alert} /> {item.richText?.text ? ( - <View style={styles.postTextContainer}> + <View + style={[ + styles.postTextContainer, + isThreadedChild && {paddingTop: 2}, + ]}> <RichText type="post-text" richText={item.richText} @@ -508,30 +520,84 @@ export const PostThreadItem = observer(function PostThreadItem({ /> </View> </View> + {item._hasMore ? ( + <Link + style={[ + styles.loadMore, + { + paddingLeft: treeView ? 44 : 70, + paddingTop: 0, + paddingBottom: treeView ? 4 : 12, + }, + ]} + href={itemHref} + title={itemTitle} + noFeedback> + <Text type="sm-medium" style={pal.textLight}> + More + </Text> + <FontAwesomeIcon + icon="angle-right" + color={pal.colors.textLight} + size={14} + /> + </Link> + ) : undefined} </PostHider> - {item._hasMore ? ( - <Link - style={[ - styles.loadMore, - {borderTopColor: pal.colors.border}, - pal.view, - ]} - href={itemHref} - title={itemTitle} - noFeedback> - <Text style={pal.link}>Continue thread...</Text> - <FontAwesomeIcon - icon="angle-right" - style={pal.link as FontAwesomeIconStyle} - size={18} - /> - </Link> - ) : undefined} - </> + </PostOuterWrapper> ) } }) +function PostOuterWrapper({ + item, + hasPrecedingItem, + treeView, + children, +}: React.PropsWithChildren<{ + item: PostThreadItemModel + hasPrecedingItem: boolean + treeView: boolean +}>) { + const {isMobile} = useWebMediaQueries() + const pal = usePalette('default') + if (treeView && item._depth > 0) { + return ( + <View + style={[ + pal.view, + styles.cursor, + {flexDirection: 'row', paddingLeft: 10}, + ]}> + {Array.from(Array(item._depth - 1)).map((_, n: number) => ( + <View + key={`${item.uri}-padding-${n}`} + style={{ + borderLeftWidth: 2, + borderLeftColor: pal.colors.border, + marginLeft: 19, + paddingLeft: isMobile ? 0 : 4, + }} + /> + ))} + <View style={{flex: 1}}>{children}</View> + </View> + ) + } + return ( + <View + style={[ + styles.outer, + pal.view, + pal.border, + item._showParentReplyLine && hasPrecedingItem && styles.noTopBorder, + styles.cursor, + ]}> + {children} + </View> + ) +} + function ExpandedPostDetails({ post, needsTranslation, @@ -600,7 +666,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', - paddingBottom: 8, + paddingBottom: 4, paddingRight: 10, }, postTextLargeContainer: { @@ -629,11 +695,10 @@ const styles = StyleSheet.create({ }, loadMore: { flexDirection: 'row', - justifyContent: 'space-between', - borderTopWidth: 1, - paddingLeft: 80, - paddingRight: 20, - paddingVertical: 12, + alignItems: 'center', + justifyContent: 'flex-start', + gap: 4, + paddingHorizontal: 20, }, replyLine: { width: 2, diff --git a/src/view/index.ts b/src/view/index.ts index da1b78146..07848aa8f 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -45,6 +45,7 @@ import {faEye} from '@fortawesome/free-solid-svg-icons/faEye' import {faEyeSlash as farEyeSlash} from '@fortawesome/free-regular-svg-icons/faEyeSlash' import {faFaceSmile} from '@fortawesome/free-regular-svg-icons/faFaceSmile' import {faFire} from '@fortawesome/free-solid-svg-icons/faFire' +import {faFlask} from '@fortawesome/free-solid-svg-icons' import {faFloppyDisk} from '@fortawesome/free-regular-svg-icons/faFloppyDisk' import {faGear} from '@fortawesome/free-solid-svg-icons/faGear' import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe' @@ -144,6 +145,7 @@ export function setup() { farEyeSlash, faFaceSmile, faFire, + faFlask, faFloppyDisk, faGear, faGlobe, diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index a6aafa530..90b98d052 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -74,6 +74,7 @@ export const PostThreadScreen = withAuthRequired(({route}: Props) => { uri={uri} view={view} onPressReply={onPressReply} + treeView={store.preferences.threadTreeViewEnabled} /> </View> {isMobile && ( diff --git a/src/view/screens/PreferencesHomeFeed.tsx b/src/view/screens/PreferencesHomeFeed.tsx index 34139bec1..404d006f8 100644 --- a/src/view/screens/PreferencesHomeFeed.tsx +++ b/src/view/screens/PreferencesHomeFeed.tsx @@ -1,6 +1,7 @@ import React, {useState} from 'react' import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Slider} from '@miblanchard/react-native-slider' import {Text} from '../com/util/text/Text' import {useStores} from 'state/index' @@ -158,11 +159,12 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({ <View style={[pal.viewLight, styles.card]}> <Text type="title-sm" style={[pal.text, s.pb5]}> - Show Posts from My Feeds (Experimental) + <FontAwesomeIcon icon="flask" color={pal.colors.text} /> Show + Posts from My Feeds </Text> <Text style={[pal.text, s.pb10]}> Set this setting to "Yes" to show samples of your saved feeds in - your following feed. + your following feed. This is an experimental feature. </Text> <ToggleButton type="default-light" diff --git a/src/view/screens/PreferencesThreads.tsx b/src/view/screens/PreferencesThreads.tsx index 731a98d71..74b28267d 100644 --- a/src/view/screens/PreferencesThreads.tsx +++ b/src/view/screens/PreferencesThreads.tsx @@ -1,6 +1,7 @@ import React from 'react' import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Text} from '../com/util/text/Text' import {useStores} from 'state/index' import {s, colors} from 'lib/styles' @@ -78,6 +79,23 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({ onPress={store.preferences.toggleThreadFollowedUsersFirst} /> </View> + + <View style={[pal.viewLight, styles.card]}> + <Text type="title-sm" style={[pal.text, s.pb5]}> + <FontAwesomeIcon icon="flask" color={pal.colors.text} /> Threaded + Mode + </Text> + <Text style={[pal.text, s.pb10]}> + Set this setting to "Yes" to show replies in a threaded view. This + is an experimental feature. + </Text> + <ToggleButton + type="default-light" + label={store.preferences.threadTreeViewEnabled ? 'Yes' : 'No'} + isSelected={store.preferences.threadTreeViewEnabled} + onPress={store.preferences.toggleThreadTreeViewEnabled} + /> + </View> </View> </ScrollView> |