about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-09-19 19:08:11 -0700
committerGitHub <noreply@github.com>2023-09-19 19:08:11 -0700
commit1af8e83d536cf6a9db128409c8e00a0b44d9a985 (patch)
tree13b8bfcee3a7f6942b1ed0cb320d0cdb1f09495e
parentd2c253a284b3341e92ae104e49f2584602795575 (diff)
downloadvoidsky-1af8e83d536cf6a9db128409c8e00a0b44d9a985.tar.zst
Tree view threads experiment (#1480)
* Add tree-view experiment to threads

* Fix typo

* Remove extra minimalshellmode call

* Fix to parent line rendering

* Fix extra border

* Some ui cleanup
-rw-r--r--src/state/models/ui/preferences.ts15
-rw-r--r--src/view/com/post-thread/PostThread.tsx48
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx141
-rw-r--r--src/view/index.ts2
-rw-r--r--src/view/screens/PostThread.tsx1
-rw-r--r--src/view/screens/PreferencesHomeFeed.tsx6
-rw-r--r--src/view/screens/PreferencesThreads.tsx18
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>