From 0582f19e7c2c6f916b427d5ecfbbb571178ce841 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Fri, 6 Aug 2021 20:18:27 -0400
Subject: Add tree-style thread display

---
 src/components/conversation/conversation.vue | 59 +++++++++++++++++++---------
 1 file changed, 41 insertions(+), 18 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 3fb26d92..cea5f88f 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -18,24 +18,47 @@
         {{ $t('timeline.collapse') }}
       </button>
     </div>
-    <status
-      v-for="status in conversation"
-      :key="status.id"
-      ref="statusComponent"
-      :inline-expanded="collapsable && isExpanded"
-      :statusoid="status"
-      :expandable="!isExpanded"
-      :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
-      :focused="focused(status.id)"
-      :in-conversation="isExpanded"
-      :highlight="getHighlight()"
-      :replies="getReplies(status.id)"
-      :in-profile="inProfile"
-      :profile-user-id="profileUserId"
-      class="conversation-status status-fadein panel-body"
-      @goto="setHighlight"
-      @toggleExpanded="toggleExpanded"
-    />
+    <div v-if="isTreeView">
+      <thread-tree
+        v-for="status in topLevel"
+        :key="status.id"
+        ref="statusComponent"
+
+        :status="status"
+        :in-profile="inProfile"
+        :conversation="conversation"
+        :collapsable="collapsable"
+        :is-expanded="isExpanded"
+        :pinned-status-ids-object="pinnedStatusIdsObject"
+        :profile-user-id="profileUserId"
+
+        :focused="focused"
+        :get-replies="getReplies"
+        :get-highlight="getHighlight"
+        :set-highlight="setHighlight"
+        :toggle-expanded="toggleExpanded"
+      />
+    </div>
+    <div v-if="isLinearView">
+      <status
+        v-for="status in conversation"
+        :key="status.id"
+        ref="statusComponent"
+        :inline-expanded="collapsable && isExpanded"
+        :statusoid="status"
+        :expandable="!isExpanded"
+        :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
+        :focused="focused(status.id)"
+        :in-conversation="isExpanded"
+        :highlight="getHighlight()"
+        :replies="getReplies(status.id)"
+        :in-profile="inProfile"
+        :profile-user-id="profileUserId"
+        class="conversation-status status-fadein panel-body"
+        @goto="setHighlight"
+        @toggleExpanded="toggleExpanded"
+      />
+    </div>
   </div>
   <div
     v-else
-- 
cgit v1.2.3-70-g09d2


From 0f2fd8a3523e9e2cd1ca6fe287eb7304895f2cba Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sat, 7 Aug 2021 00:33:06 -0400
Subject: Implement thread folding/expanding

---
 src/components/conversation/conversation.js  | 105 +++++++++++++++++++++++++--
 src/components/conversation/conversation.vue |   7 ++
 src/components/status/status.js              |  22 +++++-
 src/components/status/status.vue             |  13 ++++
 src/components/thread_tree/thread_tree.js    |  12 ++-
 src/components/thread_tree/thread_tree.vue   |  35 ++++++++-
 6 files changed, 180 insertions(+), 14 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index e663ba15..0abb7abd 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -38,7 +38,8 @@ const conversation = {
   data () {
     return {
       highlight: null,
-      expanded: false
+      expanded: false,
+      threadDisplayStatusObject: {} // id => 'showing' | 'hidden'
     }
   },
   props: [
@@ -56,6 +57,9 @@ const conversation = {
     }
   },
   computed: {
+    maxDepthToShowByDefault () {
+      return 4
+    },
     displayStyle () {
       return this.$store.state.config.conversationDisplay
     },
@@ -112,15 +116,14 @@ const conversation = {
         const id = cur.id
         a.forest[id] = this.getReplies(id)
           .map(s => s.id)
-          .sort((a, b) => reverseLookupTable[a] - reverseLookupTable[b])
 
-        a.topLevel = a.topLevel.filter(k => a.forest[id].contains(k))
         return a
       }, {
         forest: {},
-        topLevel: this.conversation.map(s => s.id)
       })
 
+      debug('threads = ', threads)
+
       const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
         if (processed[id]) {
           return []
@@ -131,18 +134,63 @@ const conversation = {
           status: this.conversation[reverseLookupTable[id]],
           id,
           depth
-        }, walk(forest, forest[child], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
+        }, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
       }).reduce((a, b) => a.concat(b), [])
 
-      const linearized = walk(threads.forest, threads.topLevel)
+      const linearized = walk(threads.forest, this.topLevel.map(k => k.id))
 
       return linearized
     },
+    replyIds () {
+      return this.conversation.map(k => k.id)
+        .reduce((res, id) => {
+          res[id] = (this.replies[id] || []).map(k => k.id)
+          return res
+        }, {})
+    },
+    totalReplyCount () {
+      debug('replyIds=', this.replyIds)
+      const sizes = {}
+      const subTreeSizeFor = (id) => {
+        if (sizes[id]) {
+          return sizes[id]
+        }
+        sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0)
+        return sizes[id]
+      }
+      this.conversation.map(k => k.id).map(subTreeSizeFor)
+      debug('totalReplyCount=', sizes)
+      return Object.keys(sizes).reduce((res, id) => {
+        res[id] = sizes[id] - 1 // exclude itself
+        return res
+      }, {})
+    },
+    totalReplyDepth () {
+      const depths = {}
+      const subTreeDepthFor = (id) => {
+        if (depths[id]) {
+          return depths[id]
+        }
+        depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0)
+        return depths[id]
+      }
+      this.conversation.map(k => k.id).map(subTreeDepthFor)
+      return Object.keys(depths).reduce((res, id) => {
+        res[id] = depths[id] - 1 // exclude itself
+        return res
+      }, {})
+    },
+    depths () {
+      debug('threadTree', this.threadTree)
+      return this.threadTree.reduce((a, k) => {
+        a[k.id] = k.depth
+        return a
+      }, {})
+    },
     topLevel () {
       const topLevel = this.conversation.reduce((tl, cur) =>
         tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation)
       debug("toplevel =", topLevel)
-      debug("toplevel =", topLevel)
       return topLevel
     },
     replies () {
@@ -169,6 +217,25 @@ const conversation = {
     hiddenStyle () {
       const height = (this.status && this.status.virtualHeight) || '120px'
       return this.virtualHidden ? { height } : {}
+    },
+    threadDisplayStatus () {
+      return this.conversation.reduce((a, k) => {
+        const id = k.id
+        const depth = this.depths[id]
+        const status = (() => {
+          if (this.threadDisplayStatusObject[id]) {
+            return this.threadDisplayStatusObject[id]
+          }
+          if (depth <= this.maxDepthToShowByDefault) {
+            return 'showing'
+          } else {
+            return 'hidden'
+          }
+        })()
+
+        a[id] = status
+        return a
+      }, {})
     }
   },
   components: {
@@ -235,6 +302,30 @@ const conversation = {
     getConversationId (statusId) {
       const status = this.$store.state.statuses.allStatusesObject[statusId]
       return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
+    },
+    setThreadDisplay (id, nextStatus) {
+      this.threadDisplayStatusObject = {
+        ...this.threadDisplayStatusObject,
+        [id]: nextStatus
+      }
+    },
+    toggleThreadDisplay (id) {
+      const depth = this.depths[id]
+      debug('depth = ', depth)
+      debug(
+        'threadDisplayStatus = ', this.threadDisplayStatus,
+        'threadDisplayStatusObject = ', this.threadDisplayStatusObject)
+      const curStatus = this.threadDisplayStatus[id]
+      const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
+      debug('toggling', id, 'to', nextStatus)
+      this.setThreadDisplay(id, nextStatus)
+    },
+    setThreadDisplayRecursively (id, nextStatus) {
+      this.setThreadDisplay(id, nextStatus)
+      this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus))
+    },
+    showThreadRecursively (id) {
+      this.setThreadDisplayRecursively(id, 'showing')
     }
   }
 }
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index cea5f88f..783cc894 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -23,6 +23,7 @@
         v-for="status in topLevel"
         :key="status.id"
         ref="statusComponent"
+        :depth="0"
 
         :status="status"
         :in-profile="inProfile"
@@ -37,6 +38,12 @@
         :get-highlight="getHighlight"
         :set-highlight="setHighlight"
         :toggle-expanded="toggleExpanded"
+
+        :toggle-thread-display="toggleThreadDisplay"
+        :thread-display-status="threadDisplayStatus"
+        :show-thread-recursively="showThreadRecursively"
+        :total-reply-count="totalReplyCount"
+        :total-reply-depth="totalReplyDepth"
       />
     </div>
     <div v-if="isLinearView">
diff --git a/src/components/status/status.js b/src/components/status/status.js
index d8f94926..9d423631 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -35,7 +35,9 @@ import {
   faStar,
   faEyeSlash,
   faEye,
-  faThumbtack
+  faThumbtack,
+  faAngleDoubleUp,
+  faAngleDoubleDown
 } from '@fortawesome/free-solid-svg-icons'
 
 library.add(
@@ -52,7 +54,9 @@ library.add(
   faEllipsisH,
   faEyeSlash,
   faEye,
-  faThumbtack
+  faThumbtack,
+  faAngleDoubleUp,
+  faAngleDoubleDown
 )
 
 const Status = {
@@ -89,7 +93,10 @@ const Status = {
     'inlineExpanded',
     'showPinned',
     'inProfile',
-    'profileUserId'
+    'profileUserId',
+
+    'controlledThreadDisplayStatus',
+    'controlledToggleThreadDisplay'
   ],
   data () {
     return {
@@ -304,6 +311,12 @@ const Status = {
     },
     isSuspendable () {
       return !this.replying && this.mediaPlaying.length === 0
+    },
+    inThreadForest () {
+      return !!this.controlledThreadDisplayStatus
+    },
+    threadShowing () {
+      return this.controlledThreadDisplayStatus === 'showing'
     }
   },
   methods: {
@@ -353,6 +366,9 @@ const Status = {
     },
     setHeadTailLinks (headTailLinks) {
       this.headTailLinks = headTailLinks
+    },
+    toggleThreadDisplay () {
+      this.controlledToggleThreadDisplay()
     }
   },
   watch: {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 3bb29db6..2ebf5638 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -219,6 +219,19 @@
                     class="fa-scale-110"
                   />
                 </button>
+                <button
+                  v-if="inThreadForest && replies && replies.length"
+                  class="button-unstyled"
+                  :title="threadShowing ? $t('status.thread_hide') : $t('status.thread_show')"
+                  :aria-expanded="threadShowing ? 'true' : 'false'"
+                  @click.prevent="toggleThreadDisplay"
+                >
+                  <FAIcon
+                    fixed-width
+                    class="fa-scale-110"
+                    :icon="threadShowing ? 'angle-double-up' : 'angle-double-down'"
+                  />
+                </button>
               </span>
             </div>
             <div
diff --git a/src/components/thread_tree/thread_tree.js b/src/components/thread_tree/thread_tree.js
index 4f95122a..88b60109 100644
--- a/src/components/thread_tree/thread_tree.js
+++ b/src/components/thread_tree/thread_tree.js
@@ -21,7 +21,14 @@ const ThreadTree = {
     getHighlight: Function,
     getReplies: Function,
     setHighlight: Function,
-    toggleExpanded: Function
+    toggleExpanded: Function,
+
+    // to control display of the whole thread forest
+    toggleThreadDisplay: Function,
+    threadDisplayStatus: Object,
+    showThreadRecursively: Function,
+    totalReplyCount: Object,
+    totalReplyDepth: Object
   },
   computed: {
     reverseLookupTable () {
@@ -35,6 +42,9 @@ const ThreadTree = {
       debug('getReplies:', this.getReplies(this.status.id))
       return this.getReplies(this.status.id).map(({ id }) => this.statusById(id))
     },
+    threadShowing () {
+      return this.threadDisplayStatus[this.status.id] === 'showing'
+    }
   },
   methods: {
     statusById (id) {
diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue
index 8256eee6..9f591585 100644
--- a/src/components/thread_tree/thread_tree.vue
+++ b/src/components/thread_tree/thread_tree.vue
@@ -13,18 +13,23 @@
       :replies="getReplies(status.id)"
       :in-profile="inProfile"
       :profile-user-id="profileUserId"
-      class="conversation-status status-fadein panel-body"
+      class="conversation-status conversation-status-treeview status-fadein panel-body"
+
+      :controlled-thread-display-status="threadDisplayStatus[status.id]"
+      :controlled-toggle-thread-display="() => toggleThreadDisplay(status.id)"
+
       @goto="setHighlight"
       @toggleExpanded="toggleExpanded"
     />
     <div
-      v-if="currentReplies.length"
+      v-if="currentReplies.length && threadShowing"
       class="thread-tree-replies"
     >
       <thread-tree
         v-for="replyStatus in currentReplies"
         :key="replyStatus.id"
         ref="childComponent"
+        :depth="depth + 1"
         :status="replyStatus"
 
         :in-profile="inProfile"
@@ -40,16 +45,40 @@
         :set-highlight="setHighlight"
         :toggle-expanded="toggleExpanded"
 
-        class="conversation-status status-fadein panel-body"
+        :toggle-thread-display="toggleThreadDisplay"
+        :thread-display-status="threadDisplayStatus"
+        :show-thread-recursively="showThreadRecursively"
+        :total-reply-count="totalReplyCount"
+        :total-reply-depth="totalReplyDepth"
       />
     </div>
+    <div
+      v-if="currentReplies.length && !threadShowing"
+      class="thread-tree-replies thread-tree-replies-hidden"
+    >
+      <button
+        class="button-unstyled -link thread-tree-show-replies-button"
+        @click="showThreadRecursively(status.id)"
+      >
+        {{ $t('status.thread_show_full', { numStatus: totalReplyCount[status.id], depth: totalReplyDepth[status.id] }) }}
+      </button>
+    </div>
   </div>
 </template>
 
 <script src="./thread_tree.js"></script>
 
 <style lang="scss">
+@import '../../_variables.scss';
 .thread-tree-replies {
   margin-left: 1em;
 }
+.thread-tree-replies-hidden {
+  padding: 1em;
+  border-bottom: 1px solid var(--border, #222);
+}
+.conversation-status.conversation-status-treeview:last-child,
+.Conversation.-expanded .conversation-status.conversation-status-treeview:last-child {
+  border-bottom: 1px solid var(--border, #222);
+}
 </style>
-- 
cgit v1.2.3-70-g09d2


From ace1f5067c90be2fa0b8da22d39b0e2c88f590fb Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sat, 7 Aug 2021 10:28:45 -0400
Subject: Make status display controlled

---
 src/components/conversation/conversation.js     | 37 +++++++++++++++-
 src/components/conversation/conversation.vue    |  3 ++
 src/components/status/status.js                 |  9 +++-
 src/components/status/status.vue                |  6 +++
 src/components/status_content/status_content.js | 59 ++++++++++++++++++++++++-
 src/components/thread_tree/thread_tree.js       | 11 ++++-
 src/components/thread_tree/thread_tree.vue      | 10 +++++
 7 files changed, 130 insertions(+), 5 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 0abb7abd..6fc86b2c 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -2,7 +2,8 @@ import { reduce, filter, findIndex, clone, get } from 'lodash'
 import Status from '../status/status.vue'
 import ThreadTree from '../thread_tree/thread_tree.vue'
 
-const debug = console.log
+// const debug = console.log
+const debug = () => {}
 
 const sortById = (a, b) => {
   const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
@@ -39,7 +40,8 @@ const conversation = {
     return {
       highlight: null,
       expanded: false,
-      threadDisplayStatusObject: {} // id => 'showing' | 'hidden'
+      threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
+      statusContentPropertiesObject: {}
     }
   },
   props: [
@@ -236,6 +238,25 @@ const conversation = {
         a[id] = status
         return a
       }, {})
+    },
+    statusContentProperties () {
+      return this.conversation.reduce((a, k) => {
+        const id = k.id
+        const depth = this.depths[id]
+        const props = (() => {
+          if (this.statusContentPropertiesObject[id]) {
+            return this.statusContentPropertiesObject[id]
+          }
+          return {
+            showingTall: false,
+            expandingSubject: false,
+            showingLongSubject: false,
+          }
+        })()
+
+        a[id] = props
+        return a
+      }, {})
     }
   },
   components: {
@@ -326,6 +347,18 @@ const conversation = {
     },
     showThreadRecursively (id) {
       this.setThreadDisplayRecursively(id, 'showing')
+    },
+    setStatusContentProperty (id, name, value) {
+      this.statusContentPropertiesObject = {
+        ...this.statusContentPropertiesObject,
+        [id]: {
+          ...this.statusContentPropertiesObject[id],
+          [name]: value
+        }
+      }
+    },
+    toggleStatusContentProperty (id, name) {
+      this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
     }
   }
 }
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 783cc894..08cb72d0 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -44,6 +44,9 @@
         :show-thread-recursively="showThreadRecursively"
         :total-reply-count="totalReplyCount"
         :total-reply-depth="totalReplyDepth"
+        :status-content-properties="statusContentProperties"
+        :set-status-content-property="setStatusContentProperty"
+        :toggle-status-content-property="toggleStatusContentProperty"
       />
     </div>
     <div v-if="isLinearView">
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 9d423631..d5ee7d4e 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -96,7 +96,14 @@ const Status = {
     'profileUserId',
 
     'controlledThreadDisplayStatus',
-    'controlledToggleThreadDisplay'
+    'controlledToggleThreadDisplay',
+
+    'controlledShowingTall',
+    'controlledToggleShowingTall',
+    'controlledExpandingSubject',
+    'controlledToggleExpandingSubject',
+    'controlledShowingLongSubject',
+    'controlledToggleShowingLongSubject'
   ],
   data () {
     return {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 2ebf5638..8fdebe44 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -319,6 +319,12 @@
             :no-heading="noHeading"
             :highlight="highlight"
             :focused="isFocused"
+            :controlled-showing-tall="controlledShowingTall"
+            :controlled-expanding-subject="controlledExpandingSubject"
+            :controlled-showing-long-subject="controlledShowingLongSubject"
+            :controlled-toggle-showing-tall="controlledToggleShowingTall"
+            :controlled-toggle-expanding-subject="controlledToggleExpandingSubject"
+            :controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject"
             @mediaplay="addMediaPlaying($event)"
             @mediapause="removeMediaPlaying($event)"
             @parseReady="setHeadTailLinks"
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index dec8914a..65ec85c4 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -23,6 +23,31 @@ library.add(
   faPollH
 )
 
+const camelCase = name => name.charAt(0).toUpperCase() + name.slice(1)
+
+const controlledOrUncontrolledGetters = list => list.reduce((res, name) => {
+  const camelized = camelCase(name)
+  const toggle = `controlledToggle${camelized}`
+  const controlledName = `controlled${camelized}`
+  const uncontrolledName = `uncontrolled${camelized}`
+  res[name] = function () {
+    return this[toggle] ? this[controlledName] : this[uncontrolledName]
+  }
+  return res
+}, {})
+
+const controlledOrUncontrolledToggle = (obj, name) => {
+  const camelized = camelCase(name)
+  const toggle = `controlledToggle${camelized}`
+  const controlledName = `controlled${camelized}`
+  const uncontrolledName = `uncontrolled${camelized}`
+  if (obj[toggle]) {
+    obj[toggle]()
+  } else {
+    obj[uncontrolledName] = !obj[uncontrolledName]
+  }
+}
+
 const StatusContent = {
   name: 'StatusContent',
   props: [
@@ -31,9 +56,22 @@ const StatusContent = {
     'focused',
     'noHeading',
     'fullContent',
-    'singleLine'
+    'singleLine',
+    'controlledShowingTall',
+    'controlledExpandingSubject',
+    'controlledToggleShowingTall',
+    'controlledToggleExpandingSubject'
   ],
+  data () {
+    return {
+      uncontrolledShowingTall: this.fullContent || (this.inConversation && this.focused),
+      uncontrolledShowingLongSubject: false,
+      // not as computed because it sets the initial state which will be changed later
+      uncontrolledExpandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
+    }
+  },
   computed: {
+    ...controlledOrUncontrolledGetters(['showingTall', 'expandingSubject', 'showingLongSubject']),
     hideAttachments () {
       return (this.mergedConfig.hideAttachments && !this.inConversation) ||
         (this.mergedConfig.hideAttachmentsInConv && this.inConversation)
@@ -71,6 +109,25 @@ const StatusContent = {
     Gallery,
     LinkPreview,
     StatusBody
+  },
+  methods: {
+    toggleShowingTall () {
+      controlledOrUncontrolledToggle(this, 'showingTall')
+    },
+    toggleExpandingSubject () {
+      controlledOrUncontrolledToggle(this, 'expandingSubject')
+    },
+    toggleShowMore () {
+      if (this.mightHideBecauseTall) {
+        this.toggleShowingTall()
+      } else if (this.mightHideBecauseSubject) {
+        this.toggleExpandingSubject()
+      }
+    },
+    setMedia () {
+      const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
+      return () => this.$store.dispatch('setMedia', attachments)
+    }
   }
 }
 
diff --git a/src/components/thread_tree/thread_tree.js b/src/components/thread_tree/thread_tree.js
index 88b60109..46245bdf 100644
--- a/src/components/thread_tree/thread_tree.js
+++ b/src/components/thread_tree/thread_tree.js
@@ -28,7 +28,10 @@ const ThreadTree = {
     threadDisplayStatus: Object,
     showThreadRecursively: Function,
     totalReplyCount: Object,
-    totalReplyDepth: Object
+    totalReplyDepth: Object,
+    statusContentProperties: Object,
+    setStatusContentProperty: Function,
+    toggleStatusContentProperty: Function
   },
   computed: {
     reverseLookupTable () {
@@ -44,6 +47,9 @@ const ThreadTree = {
     },
     threadShowing () {
       return this.threadDisplayStatus[this.status.id] === 'showing'
+    },
+    currentProp () {
+      return this.statusContentProperties[this.status.id]
     }
   },
   methods: {
@@ -55,6 +61,9 @@ const ThreadTree = {
     showThread () {
     },
     showAllSubthreads () {
+    },
+    toggleCurrentProp (name) {
+      this.toggleStatusContentProperty(this.status.id, name)
     }
   }
 }
diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue
index bbdda6fb..d7077bfd 100644
--- a/src/components/thread_tree/thread_tree.vue
+++ b/src/components/thread_tree/thread_tree.vue
@@ -18,6 +18,13 @@
       :controlled-thread-display-status="threadDisplayStatus[status.id]"
       :controlled-toggle-thread-display="() => toggleThreadDisplay(status.id)"
 
+      :controlled-showing-tall="currentProp.showingTall"
+      :controlled-expanding-subject="currentProp.expandingSubject"
+      :controlled-showing-long-subject="currentProp.showingLongSubject"
+      :controlled-toggle-showing-tall="() => toggleCurrentProp('ShowingTall')"
+      :controlled-toggle-expanding-subject="() => toggleCurrentProp('expandingSubject')"
+      :controlled-toggle-showing-long-subject="() => toggleCurrentProp('showingLongSubject')"
+
       @goto="setHighlight"
       @toggleExpanded="toggleExpanded"
     />
@@ -50,6 +57,9 @@
         :show-thread-recursively="showThreadRecursively"
         :total-reply-count="totalReplyCount"
         :total-reply-depth="totalReplyDepth"
+        :status-content-properties="statusContentProperties"
+        :set-status-content-property="setStatusContentProperty"
+        :toggle-status-content-property="toggleStatusContentProperty"
       />
     </div>
     <div
-- 
cgit v1.2.3-70-g09d2


From 84a3cd92a3da885fe1a76809c589b8f464eed432 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sat, 7 Aug 2021 11:59:10 -0400
Subject: Support diving into one status in a conversation

---
 src/components/conversation/conversation.js  | 32 +++++++++++++++++++++++++++-
 src/components/conversation/conversation.vue | 30 +++++++++++++++++++++++++-
 src/components/status/status.js              | 13 ++++++-----
 src/components/status/status.vue             | 14 +++++++++++-
 src/components/thread_tree/thread_tree.js    |  3 ++-
 src/components/thread_tree/thread_tree.vue   | 15 +++++++++----
 6 files changed, 94 insertions(+), 13 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 6fc86b2c..584f310c 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -2,6 +2,17 @@ import { reduce, filter, findIndex, clone, get } from 'lodash'
 import Status from '../status/status.vue'
 import ThreadTree from '../thread_tree/thread_tree.vue'
 
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+  faAngleDoubleDown,
+  faAngleDoubleLeft
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+  faAngleDoubleDown,
+  faAngleDoubleLeft
+)
+
 // const debug = console.log
 const debug = () => {}
 
@@ -41,7 +52,8 @@ const conversation = {
       highlight: null,
       expanded: false,
       threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
-      statusContentPropertiesObject: {}
+      statusContentPropertiesObject: {},
+      diveRoot: null
     }
   },
   props: [
@@ -195,6 +207,18 @@ const conversation = {
       debug("toplevel =", topLevel)
       return topLevel
     },
+    showingTopLevel () {
+      if (this.diveRoot) {
+        return [this.conversation.filter(k => this.diveRoot === k.id)[0]]
+      }
+      return this.topLevel
+    },
+    diveDepth () {
+      return this.diveRoot ? this.depths[this.diveRoot] : 0
+    },
+    diveMode () {
+      return !!this.diveRoot
+    },
     replies () {
       let i = 1
       // eslint-disable-next-line camelcase
@@ -359,6 +383,12 @@ const conversation = {
     },
     toggleStatusContentProperty (id, name) {
       this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
+    },
+    diveIntoStatus (id) {
+      this.diveRoot = id
+    },
+    undive () {
+      this.diveRoot = null
     }
   }
 }
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 08cb72d0..84e8d8b2 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -18,9 +18,22 @@
         {{ $t('timeline.collapse') }}
       </button>
     </div>
+    <div
+      v-if="diveMode"
+      class="conversation-undive-box"
+    >
+      <i18n
+        path="status.show_all_conversation"
+        tag="button"
+        class="button-unstyled -link"
+        @click.prevent="undive"
+      >
+        <FAIcon icon="angle-double-left" />
+      </i18n>
+    </div>
     <div v-if="isTreeView">
       <thread-tree
-        v-for="status in topLevel"
+        v-for="status in showingTopLevel"
         :key="status.id"
         ref="statusComponent"
         :depth="0"
@@ -47,6 +60,7 @@
         :status-content-properties="statusContentProperties"
         :set-status-content-property="setStatusContentProperty"
         :toggle-status-content-property="toggleStatusContentProperty"
+        :dive="diveIntoStatus"
       />
     </div>
     <div v-if="isLinearView">
@@ -65,6 +79,16 @@
         :in-profile="inProfile"
         :profile-user-id="profileUserId"
         class="conversation-status status-fadein panel-body"
+
+        :toggle-thread-display="toggleThreadDisplay"
+        :thread-display-status="threadDisplayStatus"
+        :show-thread-recursively="showThreadRecursively"
+        :total-reply-count="totalReplyCount"
+        :total-reply-depth="totalReplyDepth"
+        :status-content-properties="statusContentProperties"
+        :set-status-content-property="setStatusContentProperty"
+        :toggle-status-content-property="toggleStatusContentProperty"
+
         @goto="setHighlight"
         @toggleExpanded="toggleExpanded"
       />
@@ -82,6 +106,10 @@
 @import '../../_variables.scss';
 
 .Conversation {
+  .conversation-undive-box {
+    padding: 1em;
+  }
+  .conversation-undive-box,
   .conversation-status {
     border-bottom-width: 1px;
     border-bottom-style: solid;
diff --git a/src/components/status/status.js b/src/components/status/status.js
index d5ee7d4e..f119f42e 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -36,8 +36,9 @@ import {
   faEyeSlash,
   faEye,
   faThumbtack,
-  faAngleDoubleUp,
-  faAngleDoubleDown
+  faChevronUp,
+  faChevronDown,
+  faAngleDoubleRight
 } from '@fortawesome/free-solid-svg-icons'
 
 library.add(
@@ -55,8 +56,9 @@ library.add(
   faEyeSlash,
   faEye,
   faThumbtack,
-  faAngleDoubleUp,
-  faAngleDoubleDown
+  faChevronUp,
+  faChevronDown,
+  faAngleDoubleRight
 )
 
 const Status = {
@@ -103,7 +105,8 @@ const Status = {
     'controlledExpandingSubject',
     'controlledToggleExpandingSubject',
     'controlledShowingLongSubject',
-    'controlledToggleShowingLongSubject'
+    'controlledToggleShowingLongSubject',
+    'dive'
   ],
   data () {
     return {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 8fdebe44..c2df6021 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -229,7 +229,19 @@
                   <FAIcon
                     fixed-width
                     class="fa-scale-110"
-                    :icon="threadShowing ? 'angle-double-up' : 'angle-double-down'"
+                    :icon="threadShowing ? 'chevron-up' : 'chevron-down'"
+                  />
+                </button>
+                <button
+                  v-if="dive"
+                  class="button-unstyled"
+                  :title="$t('status.show_only_conversation_under_this')"
+                  @click.prevent="dive"
+                >
+                  <FAIcon
+                    fixed-width
+                    class="fa-scale-110"
+                    :icon="'angle-double-right'"
                   />
                 </button>
               </span>
diff --git a/src/components/thread_tree/thread_tree.js b/src/components/thread_tree/thread_tree.js
index 46245bdf..3e8eedb1 100644
--- a/src/components/thread_tree/thread_tree.js
+++ b/src/components/thread_tree/thread_tree.js
@@ -31,7 +31,8 @@ const ThreadTree = {
     totalReplyDepth: Object,
     statusContentProperties: Object,
     setStatusContentProperty: Function,
-    toggleStatusContentProperty: Function
+    toggleStatusContentProperty: Function,
+    dive: Function
   },
   computed: {
     reverseLookupTable () {
diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue
index d7077bfd..adf7bcdf 100644
--- a/src/components/thread_tree/thread_tree.vue
+++ b/src/components/thread_tree/thread_tree.vue
@@ -24,6 +24,7 @@
       :controlled-toggle-showing-tall="() => toggleCurrentProp('ShowingTall')"
       :controlled-toggle-expanding-subject="() => toggleCurrentProp('expandingSubject')"
       :controlled-toggle-showing-long-subject="() => toggleCurrentProp('showingLongSubject')"
+      :dive="dive ? () => dive(status.id) : undefined"
 
       @goto="setHighlight"
       @toggleExpanded="toggleExpanded"
@@ -60,18 +61,24 @@
         :status-content-properties="statusContentProperties"
         :set-status-content-property="setStatusContentProperty"
         :toggle-status-content-property="toggleStatusContentProperty"
+        :dive="dive"
       />
     </div>
     <div
       v-if="currentReplies.length && !threadShowing"
       class="thread-tree-replies thread-tree-replies-hidden"
     >
-      <button
+      <i18n
+        tag="button"
+        path="status.thread_show_full_with_icon"
         class="button-unstyled -link thread-tree-show-replies-button"
-        @click="showThreadRecursively(status.id)"
+        @click.prevent="showThreadRecursively(status.id)"
       >
-        {{ $tc('status.thread_show_full', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id], depth: totalReplyDepth[status.id] }) }}
-      </button>
+        <FAIcon place="icon" icon="angle-double-down" />
+        <span place="text">
+          {{ $tc('status.thread_show_full', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id], depth: totalReplyDepth[status.id] }) }}
+        </span>
+      </i18n>
     </div>
   </div>
 </template>
-- 
cgit v1.2.3-70-g09d2


From d15d24c11c57ecfc49705af648b1e8f73caec51e Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sat, 7 Aug 2021 14:11:34 -0400
Subject: Add dive functionality

---
 src/components/conversation/conversation.js  | 69 ++++++++++++++++++++++++----
 src/components/conversation/conversation.vue | 15 +++++-
 src/components/status/status.vue             |  2 +-
 src/components/thread_tree/thread_tree.js    |  3 +-
 src/components/thread_tree/thread_tree.vue   |  5 +-
 5 files changed, 80 insertions(+), 14 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 584f310c..b2af1d6c 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -5,12 +5,14 @@ import ThreadTree from '../thread_tree/thread_tree.vue'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
   faAngleDoubleDown,
-  faAngleDoubleLeft
+  faAngleDoubleLeft,
+  faChevronLeft
 } from '@fortawesome/free-solid-svg-icons'
 
 library.add(
   faAngleDoubleDown,
-  faAngleDoubleLeft
+  faAngleDoubleLeft,
+  faChevronLeft
 )
 
 // const debug = console.log
@@ -53,7 +55,7 @@ const conversation = {
       expanded: false,
       threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
       statusContentPropertiesObject: {},
-      diveRoot: null
+      diveHistory: []
     }
   },
   props: [
@@ -120,6 +122,14 @@ const conversation = {
 
       return sortAndFilterConversation(conversation, this.status)
     },
+    conversationDive () {
+    },
+    statusMap () {
+      return this.conversation.reduce((res, s) => {
+        res[s.id] = s
+        return res
+      }, {})
+    },
     threadTree () {
       const reverseLookupTable = this.conversation.reduce((table, status, index) => {
         table[status.id] = index
@@ -208,16 +218,19 @@ const conversation = {
       return topLevel
     },
     showingTopLevel () {
-      if (this.diveRoot) {
-        return [this.conversation.filter(k => this.diveRoot === k.id)[0]]
+      if (this.canDive && this.diveRoot) {
+        return [this.statusMap[this.diveRoot]]
       }
       return this.topLevel
     },
+    diveRoot () {
+      return this.diveHistory[this.diveHistory.length - 1]
+    },
     diveDepth () {
-      return this.diveRoot ? this.depths[this.diveRoot] : 0
+      return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
     },
     diveMode () {
-      return !!this.diveRoot
+      return this.canDive && !!this.diveRoot
     },
     replies () {
       let i = 1
@@ -252,7 +265,7 @@ const conversation = {
           if (this.threadDisplayStatusObject[id]) {
             return this.threadDisplayStatusObject[id]
           }
-          if (depth <= this.maxDepthToShowByDefault) {
+          if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) {
             return 'showing'
           } else {
             return 'hidden'
@@ -281,6 +294,9 @@ const conversation = {
         a[id] = props
         return a
       }, {})
+    },
+    canDive () {
+      return this.isTreeView && this.isExpanded
     }
   },
   components: {
@@ -310,6 +326,25 @@ const conversation = {
     }
   },
   methods: {
+    conversationFetched () {
+      if (!this.isExpanded) {
+        return
+      }
+
+      if (!this._diven) {
+        if (!this.threadDisplayStatus[this.statusId]) {
+          return
+        }
+        this._diven = true
+        const parentOrSelf = this.parentOrSelf(this.originalStatusId)
+        console.log(
+          'this.threadDisplayStatus ', this.threadDisplayStatus,
+          'this.statusId', this.statusId)
+        if (this.threadDisplayStatus[this.statusId] === 'hidden') {
+          this.diveIntoStatus(parentOrSelf)
+        }
+      }
+    },
     fetchConversation () {
       if (this.status) {
         this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusId })
@@ -318,6 +353,7 @@ const conversation = {
             this.$store.dispatch('addNewStatuses', { statuses: descendants })
             this.setHighlight(this.originalStatusId)
           })
+          .then(this.conversationFetched)
       } else {
         this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId })
           .then((status) => {
@@ -385,10 +421,23 @@ const conversation = {
       this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
     },
     diveIntoStatus (id) {
-      this.diveRoot = id
+      this.diveHistory = [...this.diveHistory, id]
+    },
+    diveBack () {
+      this.diveHistory = [...this.diveHistory.slice(0, this.diveHistory.length - 1)]
     },
     undive () {
-      this.diveRoot = null
+      this.diveHistory = []
+    },
+    statusById (id) {
+      return this.statusMap[id]
+    },
+    parentOf (id) {
+      const { in_reply_to_status_id: parentId } = this.statusById(id)
+      return parentId
+    },
+    parentOrSelf (id) {
+      return this.parentOf(id) || id
     }
   }
 }
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 84e8d8b2..99bc7bcc 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -31,6 +31,19 @@
         <FAIcon icon="angle-double-left" />
       </i18n>
     </div>
+    <div
+      v-if="diveMode"
+      class="conversation-undive-box"
+    >
+      <i18n
+        path="status.return_to_last_showing"
+        tag="button"
+        class="button-unstyled -link"
+        @click.prevent="diveBack"
+      >
+        <FAIcon icon="chevron-left" />
+      </i18n>
+    </div>
     <div v-if="isTreeView">
       <thread-tree
         v-for="status in showingTopLevel"
@@ -60,7 +73,7 @@
         :status-content-properties="statusContentProperties"
         :set-status-content-property="setStatusContentProperty"
         :toggle-status-content-property="toggleStatusContentProperty"
-        :dive="diveIntoStatus"
+        :dive="canDive ? diveIntoStatus : undefined"
       />
     </div>
     <div v-if="isLinearView">
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index c2df6021..47d35de8 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -220,7 +220,7 @@
                   />
                 </button>
                 <button
-                  v-if="inThreadForest && replies && replies.length"
+                  v-if="inThreadForest && replies && replies.length && toggleThreadDisplay"
                   class="button-unstyled"
                   :title="threadShowing ? $t('status.thread_hide') : $t('status.thread_show')"
                   :aria-expanded="threadShowing ? 'true' : 'false'"
diff --git a/src/components/thread_tree/thread_tree.js b/src/components/thread_tree/thread_tree.js
index 3e8eedb1..ec7d5884 100644
--- a/src/components/thread_tree/thread_tree.js
+++ b/src/components/thread_tree/thread_tree.js
@@ -1,6 +1,7 @@
 import Status from '../status/status.vue'
 
-const debug = console.log
+// const debug = console.log
+const debug = () => {}
 
 const ThreadTree = {
   components: {
diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue
index adf7bcdf..f0a103d1 100644
--- a/src/components/thread_tree/thread_tree.vue
+++ b/src/components/thread_tree/thread_tree.vue
@@ -74,7 +74,10 @@
         class="button-unstyled -link thread-tree-show-replies-button"
         @click.prevent="showThreadRecursively(status.id)"
       >
-        <FAIcon place="icon" icon="angle-double-down" />
+        <FAIcon
+          place="icon"
+          icon="angle-double-down"
+        />
         <span place="text">
           {{ $tc('status.thread_show_full', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id], depth: totalReplyDepth[status.id] }) }}
         </span>
-- 
cgit v1.2.3-70-g09d2


From d9a9f97751b2f51f55848652e5126700aea0f3fe Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sat, 7 Aug 2021 18:53:23 -0400
Subject: Add simple tree style navigation

---
 src/components/conversation/conversation.js       | 70 ++++++++++++++++++++---
 src/components/conversation/conversation.vue      |  3 +-
 src/components/settings_modal/tabs/general_tab.js |  2 +-
 src/components/status/status.js                   | 16 ++++--
 src/components/status/status.vue                  |  4 +-
 src/components/thread_tree/thread_tree.js         | 14 ++++-
 src/components/thread_tree/thread_tree.vue        | 22 ++++++-
 src/modules/instance.js                           |  2 +-
 8 files changed, 113 insertions(+), 20 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index b2af1d6c..a1991888 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -80,7 +80,10 @@ const conversation = {
       return this.$store.state.config.conversationDisplay
     },
     isTreeView () {
-      return this.displayStyle === 'tree'
+      return this.displayStyle === 'tree' || this.displayStyle === 'simple_tree'
+    },
+    treeViewIsSimple () {
+      return this.displayStyle === 'simple_tree'
     },
     isLinearView () {
       return this.displayStyle === 'linear'
@@ -297,6 +300,14 @@ const conversation = {
     },
     canDive () {
       return this.isTreeView && this.isExpanded
+    },
+    focused () {
+      return (id) => {
+        return (this.isExpanded) && id === this.highlight
+      }
+    },
+    maybeHighlight () {
+      return this.isExpanded ? this.highlight : null
     }
   },
   components: {
@@ -316,6 +327,9 @@ const conversation = {
     expanded (value) {
       if (value) {
         this.fetchConversation()
+      } else {
+        // if we collapse it, we should reset the dive
+        this._diven = false
       }
     },
     virtualHidden (value) {
@@ -323,6 +337,9 @@ const conversation = {
         'setVirtualHeight',
         { statusId: this.statusId, height: `${this.$el.clientHeight}px` }
       )
+    },
+    highlight (value, old) {
+      console.log('highlight:', old, ' => ', value)
     }
   },
   methods: {
@@ -341,7 +358,8 @@ const conversation = {
           'this.threadDisplayStatus ', this.threadDisplayStatus,
           'this.statusId', this.statusId)
         if (this.threadDisplayStatus[this.statusId] === 'hidden') {
-          this.diveIntoStatus(parentOrSelf)
+          this.diveIntoStatus(parentOrSelf, /* preventScroll */ true)
+          this.tryScrollTo(this.statusId)
         }
       }
     },
@@ -365,18 +383,16 @@ const conversation = {
     getReplies (id) {
       return this.replies[id] || []
     },
-    focused (id) {
-      return (this.isExpanded) && id === this.statusId
+    getHighlight () {
+      return this.isExpanded ? this.highlight : null
     },
     setHighlight (id) {
+      console.log('setHighlight', id)
       if (!id) return
       this.highlight = id
       this.$store.dispatch('fetchFavsAndRepeats', id)
       this.$store.dispatch('fetchEmojiReactionsBy', id)
     },
-    getHighlight () {
-      return this.isExpanded ? this.highlight : null
-    },
     toggleExpanded () {
       this.expanded = !this.expanded
     },
@@ -420,14 +436,52 @@ const conversation = {
     toggleStatusContentProperty (id, name) {
       this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
     },
-    diveIntoStatus (id) {
+    leastShowingAncestor (id) {
+      let cur = id
+      let parent = this.parentOf(cur)
+      while (cur) {
+        // if the parent is showing it means cur is visible
+        if (this.threadDisplayStatus[parent] === 'showing') {
+          return cur
+        }
+        parent = this.parentOf(parent)
+        cur = this.parentOf(cur)
+      }
+      // nothing found, fall back to toplevel
+      return topLevel[0].id
+    },
+    diveIntoStatus (id, preventScroll) {
       this.diveHistory = [...this.diveHistory, id]
+      if (!preventScroll) {
+        this.goToCurrent()
+      }
     },
     diveBack () {
+      const oldHighlight = this.highlight
       this.diveHistory = [...this.diveHistory.slice(0, this.diveHistory.length - 1)]
+      if (oldHighlight) {
+        this.tryScrollTo(this.leastShowingAncestor(oldHighlight))
+      }
     },
     undive () {
+      const oldHighlight = this.highlight
       this.diveHistory = []
+      if (oldHighlight) {
+        this.tryScrollTo(this.leastShowingAncestor(oldHighlight))
+      } else {
+        this.goToCurrent()
+      }
+    },
+    tryScrollTo (id) {
+      if (this.isPage) {
+        // set statusId
+        this.$router.push({ name: 'conversation', params: { id } })
+      }
+
+      this.setHighlight(id)
+    },
+    goToCurrent () {
+      this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
     },
     statusById (id) {
       return this.statusMap[id]
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 99bc7bcc..0ffd8c37 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -61,10 +61,11 @@
 
         :focused="focused"
         :get-replies="getReplies"
-        :get-highlight="getHighlight"
+        :highlight="maybeHighlight"
         :set-highlight="setHighlight"
         :toggle-expanded="toggleExpanded"
 
+        :simple="treeViewIsSimple"
         :toggle-thread-display="toggleThreadDisplay"
         :thread-display-status="threadDisplayStatus"
         :show-thread-recursively="showThreadRecursively"
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index abe8f6f7..c43abe8a 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -20,7 +20,7 @@ const GeneralTab = {
         value: mode,
         label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`)
       })),
-      conversationDisplayOptions: ['tree', 'linear'].map(mode => ({
+      conversationDisplayOptions: ['tree', 'simple_tree', 'linear'].map(mode => ({
         key: mode,
         value: mode,
         label: this.$t(`settings.conversation_display_${mode}`)
diff --git a/src/components/status/status.js b/src/components/status/status.js
index f119f42e..15c2d029 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -97,6 +97,7 @@ const Status = {
     'inProfile',
     'profileUserId',
 
+    'simpleTree',
     'controlledThreadDisplayStatus',
     'controlledToggleThreadDisplay',
 
@@ -379,10 +380,9 @@ const Status = {
     },
     toggleThreadDisplay () {
       this.controlledToggleThreadDisplay()
-    }
-  },
-  watch: {
-    'highlight': function (id) {
+    },
+    scrollIfHighlighted (highlightId) {
+      const id = highlightId
       if (this.status.id === id) {
         let rect = this.$el.getBoundingClientRect()
         if (rect.top < 100) {
@@ -396,6 +396,14 @@ const Status = {
           window.scrollBy(0, rect.bottom - window.innerHeight + 50)
         }
       }
+    }
+  },
+  mounted () {
+    this.scrollIfHighlighted(this.highlight)
+  },
+  watch: {
+    'highlight': function (id) {
+      this.scrollIfHighlighted(id)
     },
     'status.repeat_num': function (num) {
       // refetch repeats when repeat_num is changed in any way
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 47d35de8..e4812711 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -220,7 +220,7 @@
                   />
                 </button>
                 <button
-                  v-if="inThreadForest && replies && replies.length && toggleThreadDisplay"
+                  v-if="inThreadForest && replies && replies.length && !simpleTree"
                   class="button-unstyled"
                   :title="threadShowing ? $t('status.thread_hide') : $t('status.thread_show')"
                   :aria-expanded="threadShowing ? 'true' : 'false'"
@@ -233,7 +233,7 @@
                   />
                 </button>
                 <button
-                  v-if="dive"
+                  v-if="dive && !simpleTree"
                   class="button-unstyled"
                   :title="$t('status.show_only_conversation_under_this')"
                   @click.prevent="dive"
diff --git a/src/components/thread_tree/thread_tree.js b/src/components/thread_tree/thread_tree.js
index ec7d5884..cb1d2242 100644
--- a/src/components/thread_tree/thread_tree.js
+++ b/src/components/thread_tree/thread_tree.js
@@ -1,5 +1,16 @@
 import Status from '../status/status.vue'
 
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+  faAngleDoubleDown,
+  faAngleDoubleRight
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+  faAngleDoubleDown,
+  faAngleDoubleRight
+)
+
 // const debug = console.log
 const debug = () => {}
 
@@ -19,11 +30,12 @@ const ThreadTree = {
     profileUserId: String,
 
     focused: Function,
-    getHighlight: Function,
+    highlight: String,
     getReplies: Function,
     setHighlight: Function,
     toggleExpanded: Function,
 
+    simple: Boolean,
     // to control display of the whole thread forest
     toggleThreadDisplay: Function,
     threadDisplayStatus: Object,
diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue
index f0a103d1..b803f80f 100644
--- a/src/components/thread_tree/thread_tree.vue
+++ b/src/components/thread_tree/thread_tree.vue
@@ -9,12 +9,13 @@
       :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
       :focused="focused(status.id)"
       :in-conversation="isExpanded"
-      :highlight="getHighlight()"
+      :highlight="highlight"
       :replies="getReplies(status.id)"
       :in-profile="inProfile"
       :profile-user-id="profileUserId"
       class="conversation-status conversation-status-treeview status-fadein panel-body"
 
+      :simple-tree="simple"
       :controlled-thread-display-status="threadDisplayStatus[status.id]"
       :controlled-toggle-thread-display="() => toggleThreadDisplay(status.id)"
 
@@ -49,10 +50,11 @@
 
         :focused="focused"
         :get-replies="getReplies"
-        :get-highlight="getHighlight"
+        :highlight="highlight"
         :set-highlight="setHighlight"
         :toggle-expanded="toggleExpanded"
 
+        :simple="simple"
         :toggle-thread-display="toggleThreadDisplay"
         :thread-display-status="threadDisplayStatus"
         :show-thread-recursively="showThreadRecursively"
@@ -69,6 +71,22 @@
       class="thread-tree-replies thread-tree-replies-hidden"
     >
       <i18n
+        v-if="simple"
+        tag="button"
+        path="status.thread_follow_with_icon"
+        class="button-unstyled -link thread-tree-show-replies-button"
+        @click.prevent="dive(status.id)"
+      >
+        <FAIcon
+          place="icon"
+          icon="angle-double-right"
+        />
+        <span place="text">
+          {{ $tc('status.thread_follow', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id] }) }}
+        </span>
+      </i18n>
+      <i18n
+        v-else
         tag="button"
         path="status.thread_show_full_with_icon"
         class="button-unstyled -link thread-tree-show-replies-button"
diff --git a/src/modules/instance.js b/src/modules/instance.js
index a160a557..8a5bbbd1 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -53,7 +53,7 @@ const defaultState = {
   theme: 'pleroma-dark',
   virtualScrolling: true,
   sensitiveByDefault: false,
-  conversationDisplay: 'tree',
+  conversationDisplay: 'simple_tree',
 
   // Nasty stuff
   customEmoji: [],
-- 
cgit v1.2.3-70-g09d2


From 61bb69c88f6d70431a72bfb649d0f796b976a83b Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sun, 8 Aug 2021 12:40:17 -0400
Subject: Optimize thread display

---
 src/_variables.scss                          |   2 +
 src/components/conversation/conversation.vue | 215 ++++++++++++++++-----------
 src/components/status/status.scss            |   9 --
 src/components/thread_tree/thread_tree.vue   |  16 +-
 4 files changed, 139 insertions(+), 103 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/_variables.scss b/src/_variables.scss
index 9004d551..099d3606 100644
--- a/src/_variables.scss
+++ b/src/_variables.scss
@@ -30,3 +30,5 @@ $fallback--attachmentRadius: 10px;
 $fallback--chatMessageRadius: 10px;
 
 $fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
+
+$status-margin: 0.75em;
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 0ffd8c37..cd108f69 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -18,94 +18,102 @@
         {{ $t('timeline.collapse') }}
       </button>
     </div>
-    <div
-      v-if="diveMode"
-      class="conversation-undive-box"
-    >
-      <i18n
-        path="status.show_all_conversation"
-        tag="button"
-        class="button-unstyled -link"
-        @click.prevent="undive"
+    <div class="conversation-body panel-body">
+      <div
+        v-if="diveMode"
+        class="conversation-undive-box"
       >
-        <FAIcon icon="angle-double-left" />
-      </i18n>
-    </div>
-    <div
-      v-if="diveMode"
-      class="conversation-undive-box"
-    >
-      <i18n
-        path="status.return_to_last_showing"
-        tag="button"
-        class="button-unstyled -link"
-        @click.prevent="diveBack"
+        <i18n
+          path="status.show_all_conversation"
+          tag="button"
+          class="button-unstyled -link"
+          @click.prevent="undive"
+        >
+          <FAIcon icon="angle-double-left" />
+        </i18n>
+      </div>
+      <div
+        v-if="diveMode"
+        class="conversation-undive-box"
       >
-        <FAIcon icon="chevron-left" />
-      </i18n>
-    </div>
-    <div v-if="isTreeView">
-      <thread-tree
-        v-for="status in showingTopLevel"
-        :key="status.id"
-        ref="statusComponent"
-        :depth="0"
+        <i18n
+          path="status.return_to_last_showing"
+          tag="button"
+          class="button-unstyled -link"
+          @click.prevent="diveBack"
+        >
+          <FAIcon icon="chevron-left" />
+        </i18n>
+      </div>
+      <div
+        v-if="isTreeView"
+        class="thread-body"
+      >
+        <thread-tree
+          v-for="status in showingTopLevel"
+          :key="status.id"
+          ref="statusComponent"
+          :depth="0"
 
-        :status="status"
-        :in-profile="inProfile"
-        :conversation="conversation"
-        :collapsable="collapsable"
-        :is-expanded="isExpanded"
-        :pinned-status-ids-object="pinnedStatusIdsObject"
-        :profile-user-id="profileUserId"
+          :status="status"
+          :in-profile="inProfile"
+          :conversation="conversation"
+          :collapsable="collapsable"
+          :is-expanded="isExpanded"
+          :pinned-status-ids-object="pinnedStatusIdsObject"
+          :profile-user-id="profileUserId"
 
-        :focused="focused"
-        :get-replies="getReplies"
-        :highlight="maybeHighlight"
-        :set-highlight="setHighlight"
-        :toggle-expanded="toggleExpanded"
+          :focused="focused"
+          :get-replies="getReplies"
+          :highlight="maybeHighlight"
+          :set-highlight="setHighlight"
+          :toggle-expanded="toggleExpanded"
 
-        :simple="treeViewIsSimple"
-        :toggle-thread-display="toggleThreadDisplay"
-        :thread-display-status="threadDisplayStatus"
-        :show-thread-recursively="showThreadRecursively"
-        :total-reply-count="totalReplyCount"
-        :total-reply-depth="totalReplyDepth"
-        :status-content-properties="statusContentProperties"
-        :set-status-content-property="setStatusContentProperty"
-        :toggle-status-content-property="toggleStatusContentProperty"
-        :dive="canDive ? diveIntoStatus : undefined"
-      />
-    </div>
-    <div v-if="isLinearView">
-      <status
-        v-for="status in conversation"
-        :key="status.id"
-        ref="statusComponent"
-        :inline-expanded="collapsable && isExpanded"
-        :statusoid="status"
-        :expandable="!isExpanded"
-        :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
-        :focused="focused(status.id)"
-        :in-conversation="isExpanded"
-        :highlight="getHighlight()"
-        :replies="getReplies(status.id)"
-        :in-profile="inProfile"
-        :profile-user-id="profileUserId"
-        class="conversation-status status-fadein panel-body"
+          :simple="treeViewIsSimple"
+          :toggle-thread-display="toggleThreadDisplay"
+          :thread-display-status="threadDisplayStatus"
+          :show-thread-recursively="showThreadRecursively"
+          :total-reply-count="totalReplyCount"
+          :total-reply-depth="totalReplyDepth"
+          :status-content-properties="statusContentProperties"
+          :set-status-content-property="setStatusContentProperty"
+          :toggle-status-content-property="toggleStatusContentProperty"
+          :dive="canDive ? diveIntoStatus : undefined"
+        />
+      </div>
+      <div
+        v-if="isLinearView"
+        class="thread-body"
+      >
+        <status
+          v-for="status in conversation"
+          :key="status.id"
+          ref="statusComponent"
+          :inline-expanded="collapsable && isExpanded"
+          :statusoid="status"
+          :expandable="!isExpanded"
+          :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
+          :focused="focused(status.id)"
+          :in-conversation="isExpanded"
+          :highlight="getHighlight()"
+          :replies="getReplies(status.id)"
+          :in-profile="inProfile"
+          :profile-user-id="profileUserId"
+          class="conversation-status status-fadein panel-body"
 
-        :toggle-thread-display="toggleThreadDisplay"
-        :thread-display-status="threadDisplayStatus"
-        :show-thread-recursively="showThreadRecursively"
-        :total-reply-count="totalReplyCount"
-        :total-reply-depth="totalReplyDepth"
-        :status-content-properties="statusContentProperties"
-        :set-status-content-property="setStatusContentProperty"
-        :toggle-status-content-property="toggleStatusContentProperty"
+          :toggle-thread-display="toggleThreadDisplay"
+          :thread-display-status="threadDisplayStatus"
+          :show-thread-recursively="showThreadRecursively"
+          :total-reply-count="totalReplyCount"
+          :total-reply-depth="totalReplyDepth"
+          :status-content-properties="statusContentProperties"
+          :set-status-content-property="setStatusContentProperty"
+          :toggle-status-content-property="toggleStatusContentProperty"
 
-        @goto="setHighlight"
-        @toggleExpanded="toggleExpanded"
-      />
+          @goto="setHighlight"
+          @toggleExpanded="toggleExpanded"
+        />
+      </div>
     </div>
   </div>
   <div
@@ -121,21 +129,54 @@
 
 .Conversation {
   .conversation-undive-box {
-    padding: 1em;
+    padding: $status-margin;
   }
+
+  /* HACK: we want the border width to scale with the status *below it* */
   .conversation-undive-box,
   .conversation-status {
-    border-bottom-width: 1px;
-    border-bottom-style: solid;
-    border-bottom-color: var(--border, $fallback--border);
+    border-top-width: 1px;
+    border-top-style: solid;
+    border-top-color: var(--border, $fallback--border);
     border-radius: 0;
   }
 
+  &.-expanded .conversation-body .thread-tree:nth-child(1) > .conversation-status {
+    border-top-left-radius: $fallback--panelRadius;
+    border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
+  }
+
+  /* first element in conversation body */
+  &.-expanded .conversation-body {
+    .conversation-undive-box:nth-child(1),
+    & > .conversation-status:nth-child(1),
+    & > .thread-body:nth-child(1) > .thread-tree:nth-child(1) > .conversation-status:nth-child(1), {
+      border-top: none;
+    }
+  }
+
+  /* first unexpanded statuses in timeline */
+  &:first-child:not(.-expanded) {
+    .conversation-body {
+      .conversation-status {
+        border-top: none;
+      }
+    }
+  }
+
+  /* expanded conversation in timeline */
+  &.status-fadein.-expanded .thread-body {
+    border-left-width: 4px;
+    border-left-style: solid;
+    border-left-color: $fallback--cRed;
+    border-left-color: var(--cRed, $fallback--cRed);
+    border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
+    border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
+    border-bottom: 1px solid var(--border, $fallback--border);
+  }
   &.-expanded {
     .conversation-status:last-child {
       border-bottom: none;
-      border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
-      border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
     }
   }
 }
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index 2028ade9..80bc392d 100644
--- a/src/components/status/status.scss
+++ b/src/components/status/status.scss
@@ -1,7 +1,5 @@
 @import '../../_variables.scss';
 
-$status-margin: 0.75em;
-
 .Status {
   min-width: 0;
   white-space: normal;
@@ -28,13 +26,6 @@ $status-margin: 0.75em;
     --icon: var(--selectedPostIcon, $fallback--icon);
   }
 
-  &.-conversation {
-    border-left-width: 4px;
-    border-left-style: solid;
-    border-left-color: $fallback--cRed;
-    border-left-color: var(--cRed, $fallback--cRed);
-  }
-
   .gravestone {
     padding: $status-margin;
     color: $fallback--faint;
diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue
index fa1e5f86..aafad66e 100644
--- a/src/components/thread_tree/thread_tree.vue
+++ b/src/components/thread_tree/thread_tree.vue
@@ -109,14 +109,16 @@
 <style lang="scss">
 @import '../../_variables.scss';
 .thread-tree-replies {
-  margin-left: 1em;
+  margin-left: $status-margin;
+  border-left: 1px solid var(--border, $fallback--border);
+  border-top-left-radius: $fallback--panelRadius;
+  border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
+  border-bottom-left-radius: $fallback--panelRadius;
+  border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius);
 }
+
 .thread-tree-replies-hidden {
-  padding: 1em;
-  border-bottom: 1px solid var(--border, #222);
-}
-.conversation-status.conversation-status-treeview:last-child,
-.Conversation.-expanded .conversation-status.conversation-status-treeview:last-child {
-  border-bottom: 1px solid var(--border, #222);
+  padding: $status-margin;
+  border-top: 1px solid var(--border, $fallback--border);
 }
 </style>
-- 
cgit v1.2.3-70-g09d2


From 8780246ce5566e60520354f5a3de92eacd059f61 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sun, 8 Aug 2021 12:55:04 -0400
Subject: Optimize thread border radius

---
 src/components/conversation/conversation.vue | 12 +++++++++---
 src/components/thread_tree/thread_tree.vue   |  3 +--
 2 files changed, 10 insertions(+), 5 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index cd108f69..11b5b3d8 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -130,10 +130,13 @@
 .Conversation {
   .conversation-undive-box {
     padding: $status-margin;
+    border-bottom-width: 1px;
+    border-bottom-style: solid;
+    border-bottom-color: var(--border, $fallback--border);
+    border-radius: 0;
   }
 
   /* HACK: we want the border width to scale with the status *below it* */
-  .conversation-undive-box,
   .conversation-status {
     border-top-width: 1px;
     border-top-style: solid;
@@ -146,11 +149,14 @@
     border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
   }
 
-  /* first element in conversation body */
+  /* first element in a reply tree, the border is supplied by reply tree instead
+     for radius to display properly
+   */
   &.-expanded .conversation-body {
     .conversation-undive-box:nth-child(1),
     & > .conversation-status:nth-child(1),
-    & > .thread-body:nth-child(1) > .thread-tree:nth-child(1) > .conversation-status:nth-child(1), {
+    & > .thread-body:nth-child(1) > .thread-tree:nth-child(1) > .conversation-status:nth-child(1),
+    .thread-tree:nth-child(1) > .conversation-status:nth-child(1) {
       border-top: none;
     }
   }
diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue
index aafad66e..79ba0cc5 100644
--- a/src/components/thread_tree/thread_tree.vue
+++ b/src/components/thread_tree/thread_tree.vue
@@ -111,10 +111,9 @@
 .thread-tree-replies {
   margin-left: $status-margin;
   border-left: 1px solid var(--border, $fallback--border);
+  border-top: 1px solid var(--border, $fallback--border);
   border-top-left-radius: $fallback--panelRadius;
   border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
-  border-bottom-left-radius: $fallback--panelRadius;
-  border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius);
 }
 
 .thread-tree-replies-hidden {
-- 
cgit v1.2.3-70-g09d2


From 0e4a7c3d05333d1e3c86b26b4d6b7cd296ff2582 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sun, 8 Aug 2021 13:29:49 -0400
Subject: Make dive/undive button clickable along the whole row

---
 src/components/conversation/conversation.vue | 4 ++++
 src/components/thread_tree/thread_tree.vue   | 4 ++++
 2 files changed, 8 insertions(+)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 11b5b3d8..86a227ce 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -134,6 +134,10 @@
     border-bottom-style: solid;
     border-bottom-color: var(--border, $fallback--border);
     border-radius: 0;
+    /* Make the button stretch along the whole row */
+    display: flex;
+    align-items: stretch;
+    flex-direction: column;
   }
 
   /* HACK: we want the border width to scale with the status *below it* */
diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue
index 79ba0cc5..46d65101 100644
--- a/src/components/thread_tree/thread_tree.vue
+++ b/src/components/thread_tree/thread_tree.vue
@@ -119,5 +119,9 @@
 .thread-tree-replies-hidden {
   padding: $status-margin;
   border-top: 1px solid var(--border, $fallback--border);
+  /* Make the button stretch along the whole row */
+  display: flex;
+  align-items: stretch;
+  flex-direction: column;
 }
 </style>
-- 
cgit v1.2.3-70-g09d2


From 4adffb483579108c0bfe7593157e9bed3571903f Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Tue, 10 Aug 2021 21:28:13 -0400
Subject: Remove horizontal border and thicken vertical border in a thread tree

---
 src/components/conversation/conversation.vue | 32 +++++-----------------------
 src/components/thread_tree/thread_tree.vue   |  7 ++----
 2 files changed, 7 insertions(+), 32 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 86a227ce..c866b983 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -142,36 +142,14 @@
 
   /* HACK: we want the border width to scale with the status *below it* */
   .conversation-status {
-    border-top-width: 1px;
-    border-top-style: solid;
-    border-top-color: var(--border, $fallback--border);
+    border-bottom-width: 1px;
+    border-bottom-style: solid;
+    border-bottom-color: var(--border, $fallback--border);
     border-radius: 0;
   }
 
-  &.-expanded .conversation-body .thread-tree:nth-child(1) > .conversation-status {
-    border-top-left-radius: $fallback--panelRadius;
-    border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
-  }
-
-  /* first element in a reply tree, the border is supplied by reply tree instead
-     for radius to display properly
-   */
-  &.-expanded .conversation-body {
-    .conversation-undive-box:nth-child(1),
-    & > .conversation-status:nth-child(1),
-    & > .thread-body:nth-child(1) > .thread-tree:nth-child(1) > .conversation-status:nth-child(1),
-    .thread-tree:nth-child(1) > .conversation-status:nth-child(1) {
-      border-top: none;
-    }
-  }
-
-  /* first unexpanded statuses in timeline */
-  &:first-child:not(.-expanded) {
-    .conversation-body {
-      .conversation-status {
-        border-top: none;
-      }
-    }
+  &.-expanded .thread-tree .conversation-status {
+    border-bottom: none;
   }
 
   /* expanded conversation in timeline */
diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue
index 46d65101..dce03f27 100644
--- a/src/components/thread_tree/thread_tree.vue
+++ b/src/components/thread_tree/thread_tree.vue
@@ -110,15 +110,12 @@
 @import '../../_variables.scss';
 .thread-tree-replies {
   margin-left: $status-margin;
-  border-left: 1px solid var(--border, $fallback--border);
-  border-top: 1px solid var(--border, $fallback--border);
-  border-top-left-radius: $fallback--panelRadius;
-  border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
+  border-left: 2px solid var(--border, $fallback--border);
 }
 
 .thread-tree-replies-hidden {
   padding: $status-margin;
-  border-top: 1px solid var(--border, $fallback--border);
+  //border-top: 1px solid var(--border, $fallback--border);
   /* Make the button stretch along the whole row */
   display: flex;
   align-items: stretch;
-- 
cgit v1.2.3-70-g09d2


From e560fbc9352f9f8754451f38c5e3ecef6da96686 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Tue, 10 Aug 2021 23:58:27 -0400
Subject: Implement Misskey-style tree view

Now the tree will be always rooted at the highlighted status, and
all its ancestors shown linearly on the top.

Enhancement: If an ancestor has more
than one reply (i.e. it has a child that is not on current status's
ancestor chain), we are given a link to root the thread at that status.
---
 src/components/conversation/conversation.js  |  61 ++++++--------
 src/components/conversation/conversation.vue | 118 ++++++++++++++++++++++-----
 2 files changed, 124 insertions(+), 55 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index bd8315b8..817b9bda 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -55,7 +55,7 @@ const conversation = {
       expanded: false,
       threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
       statusContentPropertiesObject: {},
-      diveHistory: []
+      inlineDivePosition: null
     }
   },
   props: [
@@ -231,7 +231,10 @@ const conversation = {
       return this.topLevel
     },
     diveRoot () {
-      return this.diveHistory[this.diveHistory.length - 1]
+      (() => {})(this.conversation)
+      const statusId = this.inlineDivePosition || this.statusId
+      const isTopLevel = !this.parentOf(statusId)
+      return isTopLevel ? null : statusId
     },
     diveDepth () {
       return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
@@ -332,7 +335,6 @@ const conversation = {
         this.fetchConversation()
       } else {
         // if we collapse it, we should reset the dive
-        this._diven = false
         this.undive()
       }
     },
@@ -348,19 +350,6 @@ const conversation = {
       if (!this.isExpanded) {
         return
       }
-
-      if (!this._diven) {
-        if (!this.threadDisplayStatus[this.statusId]) {
-          return
-        }
-        this._diven = true
-        const parentOrSelf = this.parentOrSelf(this.originalStatusId)
-        // If current status is not visible
-        if (this.threadDisplayStatus[parentOrSelf] === 'hidden') {
-          this.diveIntoStatus(parentOrSelf, /* preventScroll */ true)
-          this.tryScrollTo(this.statusId)
-        }
-      }
     },
     fetchConversation () {
       if (this.status) {
@@ -449,26 +438,15 @@ const conversation = {
       return this.topLevel[0] ? this.topLevel[0].id : undefined
     },
     diveIntoStatus (id, preventScroll) {
-      this.diveHistory = [...this.diveHistory, id]
-      if (!preventScroll) {
-        this.goToCurrent()
-      }
+      this.tryScrollTo(id)
     },
-    diveBack () {
-      const oldHighlight = this.highlight
-      this.diveHistory = [...this.diveHistory.slice(0, this.diveHistory.length - 1)]
-      if (oldHighlight) {
-        this.tryScrollTo(this.leastVisibleAncestor(oldHighlight))
-      }
+    diveToTopLevel () {
+      this.tryScrollTo(this.topLevel[0].id)
     },
+    // only used when we are not on a page
     undive () {
-      const oldHighlight = this.highlight
-      this.diveHistory = []
-      if (oldHighlight) {
-        this.tryScrollTo(this.leastVisibleAncestor(oldHighlight))
-      } else {
-        this.goToCurrent()
-      }
+      this.inlineDivePosition = null
+      this.setHighlight(this.statusId)
     },
     tryScrollTo (id) {
       if (!id) {
@@ -477,8 +455,9 @@ const conversation = {
       if (this.isPage) {
         // set statusId
         this.$router.push({ name: 'conversation', params: { id } })
+      } else {
+        this.inlineDivePosition = id
       }
-
       this.setHighlight(id)
     },
     goToCurrent () {
@@ -493,10 +472,24 @@ const conversation = {
         return undefined
       }
       const { in_reply_to_status_id: parentId } = status
+      if (!this.statusMap[parentId]) {
+        return undefined
+      }
       return parentId
     },
     parentOrSelf (id) {
       return this.parentOf(id) || id
+    },
+    // Ancestors of some status, from top to bottom
+    ancestorsOf (id) {
+      const ancestors = []
+      let cur = this.parentOf(id)
+      while (cur) {
+        ancestors.unshift(this.statusMap[cur])
+        cur = this.parentOf(cur)
+      }
+      // console.log('ancestors = ', ancestors, 'conversation = ', this.conversation.map(k => k.id), 'statusContentProperties=', this.statusContentProperties)
+      return ancestors
     }
   }
 }
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index c866b983..20ce54a6 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -21,34 +21,88 @@
     <div class="conversation-body panel-body">
       <div
         v-if="diveMode"
-        class="conversation-undive-box"
+        class="conversation-dive-to-top-level-box"
       >
         <i18n
           path="status.show_all_conversation"
           tag="button"
           class="button-unstyled -link"
-          @click.prevent="undive"
+          @click.prevent="diveToTopLevel"
         >
           <FAIcon icon="angle-double-left" />
         </i18n>
       </div>
-      <div
-        v-if="diveMode"
-        class="conversation-undive-box"
-      >
-        <i18n
-          path="status.return_to_last_showing"
-          tag="button"
-          class="button-unstyled -link"
-          @click.prevent="diveBack"
-        >
-          <FAIcon icon="chevron-left" />
-        </i18n>
-      </div>
       <div
         v-if="isTreeView"
         class="thread-body"
       >
+        <div
+          v-if="ancestorsOf(diveRoot).length"
+          class="thread-ancestors"
+        >
+          <div
+            v-for="status in ancestorsOf(diveRoot)"
+            :key="status.id"
+            class="thread-ancestor"
+            :class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1}"
+          >
+            <status
+              ref="statusComponent"
+              :inline-expanded="collapsable && isExpanded"
+              :statusoid="status"
+              :expandable="!isExpanded"
+              :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
+              :focused="focused(status.id)"
+              :in-conversation="isExpanded"
+              :highlight="getHighlight()"
+              :replies="getReplies(status.id)"
+              :in-profile="inProfile"
+              :profile-user-id="profileUserId"
+              class="conversation-status status-fadein panel-body"
+
+              :simple="treeViewIsSimple"
+              :toggle-thread-display="toggleThreadDisplay"
+              :thread-display-status="threadDisplayStatus"
+              :show-thread-recursively="showThreadRecursively"
+              :total-reply-count="totalReplyCount"
+              :total-reply-depth="totalReplyDepth"
+              :dive="(!treeViewIsSimple) ? () => diveIntoStatus(status.id) : null"
+
+              :controlled-showing-tall="statusContentProperties[status.id].showingTall"
+              :controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
+              :controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject"
+              :controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
+              :controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')"
+              :controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')"
+
+              @goto="setHighlight"
+              @toggleExpanded="toggleExpanded"
+            />
+            <div
+              v-if="getReplies(status.id).length > 1"
+              class="thread-ancestor-dive-box"
+            >
+              <div
+                class="thread-ancestor-dive-box-inner"
+              >
+                <i18n
+                  tag="button"
+                  path="status.ancestor_follow_with_icon"
+                  class="button-unstyled -link thread-tree-show-replies-button"
+                  @click.prevent="diveIntoStatus(status.id)"
+                >
+                  <FAIcon
+                    place="icon"
+                    icon="angle-double-right"
+                  />
+                  <span place="text">
+                    {{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }}
+                  </span>
+                </i18n>
+              </div>
+            </div>
+          </div>
+        </div>
         <thread-tree
           v-for="status in showingTopLevel"
           :key="status.id"
@@ -128,7 +182,7 @@
 @import '../../_variables.scss';
 
 .Conversation {
-  .conversation-undive-box {
+  .conversation-dive-to-top-level-box {
     padding: $status-margin;
     border-bottom-width: 1px;
     border-bottom-style: solid;
@@ -140,6 +194,27 @@
     flex-direction: column;
   }
 
+  .thread-ancestor {
+    --link: var(--faintLink);
+    --text: var(--faint);
+    color: var(--text);
+  }
+  .thread-ancestor-dive-box {
+    padding-left: $status-margin;
+    border-bottom-width: 1px;
+    border-bottom-style: solid;
+    border-bottom-color: var(--border, $fallback--border);
+    border-radius: 0;
+    /* Make the button stretch along the whole row */
+    display: flex;
+    align-items: stretch;
+    flex-direction: column;
+  }
+  .thread-ancestor-dive-box-inner {
+    padding: $status-margin;
+    //border-left: 2px solid var(--border, $fallback--border);
+  }
+
   /* HACK: we want the border width to scale with the status *below it* */
   .conversation-status {
     border-bottom-width: 1px;
@@ -148,6 +223,7 @@
     border-radius: 0;
   }
 
+  .thread-ancestor-has-other-replies .conversation-status,
   &.-expanded .thread-tree .conversation-status {
     border-bottom: none;
   }
@@ -162,10 +238,10 @@
     border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
     border-bottom: 1px solid var(--border, $fallback--border);
   }
-  &.-expanded {
-    .conversation-status:last-child {
-      border-bottom: none;
-    }
-  }
+  /* &.-expanded { */
+  /*   .conversation-status:last-child { */
+  /*     border-bottom: none; */
+  /*   } */
+  /* } */
 }
 </style>
-- 
cgit v1.2.3-70-g09d2


From 17863f54fef4203365aa2b7ce58da3bb9bb3cc8c Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Wed, 11 Aug 2021 00:30:27 -0400
Subject: Optimise thread ancestor display style

---
 src/components/conversation/conversation.vue | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 20ce54a6..f0e118e5 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -194,7 +194,12 @@
     flex-direction: column;
   }
 
-  .thread-ancestor {
+  .thread-ancestors {
+    margin-left: $status-margin;
+    border-left: 2px solid var(--border, $fallback--border);
+  }
+
+  .thread-ancestor .StatusContent {
     --link: var(--faintLink);
     --text: var(--faint);
     color: var(--text);
-- 
cgit v1.2.3-70-g09d2


From ba8598858b4a90d25b76a515dc6a9125d2809f9d Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Wed, 11 Aug 2021 00:49:38 -0400
Subject: Optimise thread ancestor borders

---
 src/components/conversation/conversation.vue | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index f0e118e5..9aea7b20 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -220,7 +220,6 @@
     //border-left: 2px solid var(--border, $fallback--border);
   }
 
-  /* HACK: we want the border width to scale with the status *below it* */
   .conversation-status {
     border-bottom-width: 1px;
     border-bottom-style: solid;
@@ -229,10 +228,18 @@
   }
 
   .thread-ancestor-has-other-replies .conversation-status,
+  .thread-ancestor:last-child .conversation-status,
+  .thread-ancestor:last-child .thread-ancestor-dive-box,
   &.-expanded .thread-tree .conversation-status {
     border-bottom: none;
   }
 
+  .thread-ancestors + .thread-tree > .conversation-status {
+    border-top-width: 1px;
+    border-top-style: solid;
+    border-top-color: var(--border, $fallback--border);
+  }
+
   /* expanded conversation in timeline */
   &.status-fadein.-expanded .thread-body {
     border-left-width: 4px;
-- 
cgit v1.2.3-70-g09d2


From 22bdcda9c0a9869f8a09507bb60215b8a5af709a Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Fri, 13 Aug 2021 18:53:31 -0400
Subject: Make other replies button stretch along the row

---
 src/components/conversation/conversation.vue | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 9aea7b20..6c8c6ef0 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -211,9 +211,11 @@
     border-bottom-color: var(--border, $fallback--border);
     border-radius: 0;
     /* Make the button stretch along the whole row */
-    display: flex;
-    align-items: stretch;
-    flex-direction: column;
+    &, &-inner {
+      display: flex;
+      align-items: stretch;
+      flex-direction: column;
+    }
   }
   .thread-ancestor-dive-box-inner {
     padding: $status-margin;
-- 
cgit v1.2.3-70-g09d2


From 244174a32b94c1373847f0ea20bb6127de5ef222 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sun, 5 Sep 2021 11:16:48 -0400
Subject: Improve "show full conversation" interaction

Now we only show that button when there are other statuses out of sight
(other toplevel statuses exist outside of the current thread tree).
---
 src/components/conversation/conversation.js  |  8 ++++++++
 src/components/conversation/conversation.vue | 12 +++++++++---
 2 files changed, 17 insertions(+), 3 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 4c429161..423930af 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -224,6 +224,9 @@ const conversation = {
       debug('toplevel =', topLevel)
       return topLevel
     },
+    otherTopLevelCount () {
+      return this.topLevel.length - 1
+    },
     showingTopLevel () {
       if (this.canDive && this.diveRoot) {
         return [this.statusMap[this.diveRoot]]
@@ -242,6 +245,11 @@ const conversation = {
     diveMode () {
       return this.canDive && !!this.diveRoot
     },
+    shouldShowAllConversationButton () {
+      // The "show all conversation" button tells the user that there exist
+      // other toplevel statuses, so do not show it if there is only a single root
+      return this.diveMode && this.topLevel.length > 1
+    },
     replies () {
       let i = 1
       // eslint-disable-next-line camelcase
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 6c8c6ef0..f0eb88c1 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -20,16 +20,22 @@
     </div>
     <div class="conversation-body panel-body">
       <div
-        v-if="diveMode"
+        v-if="shouldShowAllConversationButton"
         class="conversation-dive-to-top-level-box"
       >
         <i18n
-          path="status.show_all_conversation"
+          path="status.show_all_conversation_with_icon"
           tag="button"
           class="button-unstyled -link"
           @click.prevent="diveToTopLevel"
         >
-          <FAIcon icon="angle-double-left" />
+          <FAIcon
+            place="icon"
+            icon="angle-double-left"
+          />
+          <span place="text">
+            {{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
+          </span>
         </i18n>
       </div>
       <div
-- 
cgit v1.2.3-70-g09d2


From 863255d52fdcbbabe45c86c7e36ebafc0f7e1c53 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sun, 5 Sep 2021 16:35:47 -0400
Subject: Make position of other replies button a pref

---
 src/components/conversation/conversation.js        |  9 +++++
 src/components/conversation/conversation.vue       |  7 ++--
 src/components/settings_modal/tabs/general_tab.js  |  5 +++
 src/components/settings_modal/tabs/general_tab.vue | 40 +++++++++++++++-------
 src/components/status/status.js                    |  1 +
 src/components/status/status.vue                   | 15 +++++++-
 src/modules/config.js                              |  2 ++
 src/modules/instance.js                            |  1 +
 8 files changed, 63 insertions(+), 17 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 423930af..d4972fbc 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -92,6 +92,15 @@ const conversation = {
     isLinearView () {
       return this.displayStyle === 'linear'
     },
+    otherRepliesButtonPosition () {
+      return this.$store.getters.mergedConfig.conversationOtherRepliesButton
+    },
+    showOtherRepliesButtonBelowStatus () {
+      return this.otherRepliesButtonPosition === 'below'
+    },
+    showOtherRepliesButtonInsideStatus () {
+      return this.otherRepliesButtonPosition === 'inside'
+    },
     hideStatus () {
       if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
         return this.virtualHidden && this.$refs.statusComponent[0].suspendable
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index f0eb88c1..b3d97075 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -66,13 +66,14 @@
               :profile-user-id="profileUserId"
               class="conversation-status status-fadein panel-body"
 
-              :simple="treeViewIsSimple"
+              :simple-tree="treeViewIsSimple"
               :toggle-thread-display="toggleThreadDisplay"
               :thread-display-status="threadDisplayStatus"
               :show-thread-recursively="showThreadRecursively"
               :total-reply-count="totalReplyCount"
               :total-reply-depth="totalReplyDepth"
-              :dive="(!treeViewIsSimple) ? () => diveIntoStatus(status.id) : null"
+              :show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
+              :dive="() => diveIntoStatus(status.id)"
 
               :controlled-showing-tall="statusContentProperties[status.id].showingTall"
               :controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
@@ -85,7 +86,7 @@
               @toggleExpanded="toggleExpanded"
             />
             <div
-              v-if="getReplies(status.id).length > 1"
+              v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
               class="thread-ancestor-dive-box"
             >
               <div
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index c43abe8a..a963d204 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -25,6 +25,11 @@ const GeneralTab = {
         value: mode,
         label: this.$t(`settings.conversation_display_${mode}`)
       })),
+      conversationOtherRepliesButtonOptions: ['below', 'inside'].map(mode => ({
+        key: mode,
+        value: mode,
+        label: this.$t(`settings.conversation_other_replies_button_${mode}`)
+      })),
       mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({
         key: mode,
         value: mode,
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index f97d92c3..d5ae7810 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -161,19 +161,33 @@
             {{ $t('settings.conversation_display') }}
           </ChoiceSetting>
         </li>
-        <li v-if="conversationDisplay !== 'linear'">
-          <label for="maxDepthInThread">
-            {{ $t('settings.max_depth_in_thread') }}
-          </label>
-          <input
-            id="maxDepthInThread"
-            path.number="maxDepthInThread"
-            class="number-input"
-            type="number"
-            min="3"
-            step="1"
-          >
-        </li>
+        <ul
+          v-if="conversationDisplay !== 'linear'"
+          class="setting-list suboptions"
+        >
+          <li>
+            <label for="maxDepthInThread">
+              {{ $t('settings.max_depth_in_thread') }}
+            </label>
+            <input
+              id="maxDepthInThread"
+              path.number="maxDepthInThread"
+              class="number-input"
+              type="number"
+              min="3"
+              step="1"
+            >
+          </li>
+          <li>
+            <ChoiceSetting
+              id="conversationOtherRepliesButton"
+              path="conversationOtherRepliesButton"
+              :options="conversationOtherRepliesButtonOptions"
+            >
+              {{ $t('settings.conversation_other_replies_button') }}
+            </ChoiceSetting>
+          </li>
+        </ul>
         <li>
           <ChoiceSetting
             id="mentionLinkDisplay"
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 15c2d029..700b9764 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -100,6 +100,7 @@ const Status = {
     'simpleTree',
     'controlledThreadDisplayStatus',
     'controlledToggleThreadDisplay',
+    'showOtherRepliesAsButton',
 
     'controlledShowingTall',
     'controlledToggleShowingTall',
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index e4812711..bc0aeaf0 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -346,7 +346,20 @@
             v-if="inConversation && !isPreview && replies && replies.length"
             class="replies"
           >
-            <span class="faint">{{ $t('status.replies_list') }}</span>
+            <button
+              v-if="showOtherRepliesAsButton"
+              class="button-unstyled -link faint"
+              :title="$tc('status.ancestor_follow', replies.length - 1, { numReplies: replies.length - 1 })"
+              @click.prevent="dive"
+            >
+              {{ $t('status.replies_list') }}
+            </button>
+            <span
+              v-else
+              class="faint"
+            >
+              {{ $t('status.replies_list') }}
+            </span>
             <StatusPopover
               v-for="reply in replies"
               :key="reply.id"
diff --git a/src/modules/config.js b/src/modules/config.js
index b3c7e5ab..a111abca 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -13,6 +13,7 @@ export const multiChoiceProperties = [
   'postContentType',
   'subjectLineBehavior',
   'conversationDisplay', // tree | linear
+  'conversationOtherRepliesButton', // below | inside
   'mentionLinkDisplay' // short | full_for_remote | full
 ]
 
@@ -84,6 +85,7 @@ export const defaultState = {
   virtualScrolling: undefined, // instance default
   sensitiveByDefault: undefined, // instance default
   conversationDisplay: undefined, // instance default
+  conversationOtherRepliesButton: undefined, // instance default
   maxDepthInThread: 6
 }
 
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 853be737..cd17c23d 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -54,6 +54,7 @@ const defaultState = {
   virtualScrolling: true,
   sensitiveByDefault: false,
   conversationDisplay: 'simple_tree',
+  conversationOtherRepliesButton: 'below',
   maxDepthInThread: 6,
 
   // Nasty stuff
-- 
cgit v1.2.3-70-g09d2


From 2a510205c3e18bc1c3ff253dc4521909857cd530 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Wed, 15 Sep 2021 23:35:17 -0400
Subject: Fix virtual scrolling for tree threading

Ref: tree-threading
---
 src/components/conversation/conversation.js  | 15 ++++++++++-----
 src/components/conversation/conversation.vue |  2 +-
 src/components/thread_tree/thread_tree.js    |  7 +++++++
 3 files changed, 18 insertions(+), 6 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index d4972fbc..1e97bbf0 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -101,13 +101,16 @@ const conversation = {
     showOtherRepliesButtonInsideStatus () {
       return this.otherRepliesButtonPosition === 'inside'
     },
-    hideStatus () {
+    suspendable () {
       if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
-        return this.virtualHidden && this.$refs.statusComponent[0].suspendable
+        return this.$refs.statusComponent.every(s => s.suspendable)
       } else {
-        return this.virtualHidden
+        return true
       }
     },
+    hideStatus () {
+      return this.virtualHidden && this.suspendable
+    },
     status () {
       return this.$store.state.statuses.allStatusesObject[this.statusId]
     },
@@ -243,7 +246,6 @@ const conversation = {
       return this.topLevel
     },
     diveRoot () {
-      (() => {})(this.conversation)
       const statusId = this.inlineDivePosition || this.statusId
       const isTopLevel = !this.parentOf(statusId)
       return isTopLevel ? null : statusId
@@ -257,7 +259,10 @@ const conversation = {
     shouldShowAllConversationButton () {
       // The "show all conversation" button tells the user that there exist
       // other toplevel statuses, so do not show it if there is only a single root
-      return this.diveMode && this.topLevel.length > 1
+      return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1
+    },
+    shouldShowAncestors () {
+      return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length
     },
     replies () {
       let i = 1
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index b3d97075..0cd74539 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -43,7 +43,7 @@
         class="thread-body"
       >
         <div
-          v-if="ancestorsOf(diveRoot).length"
+          v-if="shouldShowAncestors"
           class="thread-ancestors"
         >
           <div
diff --git a/src/components/thread_tree/thread_tree.js b/src/components/thread_tree/thread_tree.js
index cb1d2242..fd21c5bc 100644
--- a/src/components/thread_tree/thread_tree.js
+++ b/src/components/thread_tree/thread_tree.js
@@ -48,6 +48,13 @@ const ThreadTree = {
     dive: Function
   },
   computed: {
+    suspendable () {
+      const selfSuspendable = this.$refs.statusComponent ? this.$refs.statusComponent.suspendable : true
+      if (this.$refs.childComponent) {
+        return selfSuspendable && this.$refs.childComponent.every(s => s.suspendable)
+      }
+      return selfSuspendable
+    },
     reverseLookupTable () {
       return this.conversation.reduce((table, status, index) => {
         table[status.id] = index
-- 
cgit v1.2.3-70-g09d2


From 20880cdf0bd33d6c189549441ab0ee26b59abf6d Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Thu, 16 Sep 2021 00:29:14 -0400
Subject: Make replying and mediaPlaying controlled

$refs is not a reliable way to deal with child components under
tree threading as it is not reactive, but the children may change at
any time. The only good way seems to be making these states aggregated on
the conversation component.

Ref: tree-threading
---
 src/components/conversation/conversation.js  | 21 +++++++++---
 src/components/conversation/conversation.vue |  4 +++
 src/components/status/status.js              | 50 +++++++++++++++++++++++++---
 src/components/thread_tree/thread_tree.js    |  3 ++
 src/components/thread_tree/thread_tree.vue   |  4 +++
 5 files changed, 72 insertions(+), 10 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 7f9f24b5..9aa7b183 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -99,6 +99,10 @@ const conversation = {
       return this.otherRepliesButtonPosition === 'inside'
     },
     suspendable () {
+      if (this.isTreeView) {
+        return Object.entries(this.statusContentProperties)
+          .every(([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0)
+      }
       if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
         return this.$refs.statusComponent.every(s => s.suspendable)
       } else {
@@ -303,14 +307,21 @@ const conversation = {
       return this.conversation.reduce((a, k) => {
         const id = k.id
         const props = (() => {
-          if (this.statusContentPropertiesObject[id]) {
-            return this.statusContentPropertiesObject[id]
-          }
-          return {
+          const def = {
             showingTall: false,
             expandingSubject: false,
-            showingLongSubject: false
+            showingLongSubject: false,
+            isReplying: false,
+            mediaPlaying: []
+          }
+
+          if (this.statusContentPropertiesObject[id]) {
+            return {
+              ...def,
+              ...this.statusContentPropertiesObject[id]
+            }
           }
+          return def
         })()
 
         a[id] = props
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 0cd74539..4d64cf08 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -78,9 +78,13 @@
               :controlled-showing-tall="statusContentProperties[status.id].showingTall"
               :controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
               :controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject"
+              :controlled-replying="statusContentProperties[status.id].replying"
+              :controlled-media-playing="statusContentProperties[status.id].mediaPlaying"
               :controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
               :controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')"
               :controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')"
+              :controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')"
+              :controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
 
               @goto="setHighlight"
               @toggleExpanded="toggleExpanded"
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 700b9764..73fad45f 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -61,6 +61,41 @@ library.add(
   faAngleDoubleRight
 )
 
+const camelCase = name => name.charAt(0).toUpperCase() + name.slice(1)
+
+const controlledOrUncontrolledGetters = list => list.reduce((res, name) => {
+  const camelized = camelCase(name)
+  const toggle = `controlledToggle${camelized}`
+  const controlledName = `controlled${camelized}`
+  const uncontrolledName = `uncontrolled${camelized}`
+  res[name] = function () {
+    return this[toggle] ? this[controlledName] : this[uncontrolledName]
+  }
+  return res
+}, {})
+
+const controlledOrUncontrolledToggle = (obj, name) => {
+  const camelized = camelCase(name)
+  const toggle = `controlledToggle${camelized}`
+  const uncontrolledName = `uncontrolled${camelized}`
+  if (obj[toggle]) {
+    obj[toggle]()
+  } else {
+    obj[uncontrolledName] = !obj[uncontrolledName]
+  }
+}
+
+const controlledOrUncontrolledSet = (obj, name, val) => {
+  const camelized = camelCase(name)
+  const set = `controlledSet${camelized}`
+  const uncontrolledName = `uncontrolled${camelized}`
+  if (obj[set]) {
+    obj[set](val)
+  } else {
+    obj[uncontrolledName] = val
+  }
+}
+
 const Status = {
   name: 'Status',
   components: {
@@ -108,20 +143,25 @@ const Status = {
     'controlledToggleExpandingSubject',
     'controlledShowingLongSubject',
     'controlledToggleShowingLongSubject',
+    'controlledReplying',
+    'controlledToggleReplying',
+    'controlledMediaPlaying',
+    'controlledSetMediaPlaying',
     'dive'
   ],
   data () {
     return {
-      replying: false,
+      uncontrolledReplying: false,
       unmuted: false,
       userExpanded: false,
-      mediaPlaying: [],
+      uncontrolledMediaPlaying: [],
       suspendable: true,
       error: null,
       headTailLinks: null
     }
   },
   computed: {
+    ...controlledOrUncontrolledGetters(['replying', 'mediaPlaying']),
     muteWords () {
       return this.mergedConfig.muteWords
     },
@@ -351,7 +391,7 @@ const Status = {
       this.error = undefined
     },
     toggleReplying () {
-      this.replying = !this.replying
+      controlledOrUncontrolledToggle(this, 'replying')
     },
     gotoOriginal (id) {
       if (this.inConversation) {
@@ -371,10 +411,10 @@ const Status = {
       return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
     },
     addMediaPlaying (id) {
-      this.mediaPlaying.push(id)
+      controlledOrUncontrolledSet(this, 'mediaPlaying', this.mediaPlaying.concat(id))
     },
     removeMediaPlaying (id) {
-      this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id)
+      controlledOrUncontrolledSet(this, 'mediaPlaying', this.mediaPlaying.filter(mediaId => mediaId !== id))
     },
     setHeadTailLinks (headTailLinks) {
       this.headTailLinks = headTailLinks
diff --git a/src/components/thread_tree/thread_tree.js b/src/components/thread_tree/thread_tree.js
index 0e499b85..71e63725 100644
--- a/src/components/thread_tree/thread_tree.js
+++ b/src/components/thread_tree/thread_tree.js
@@ -80,6 +80,9 @@ const ThreadTree = {
     },
     toggleCurrentProp (name) {
       this.toggleStatusContentProperty(this.status.id, name)
+    },
+    setCurrentProp (name, newVal) {
+      this.setStatusContentProperty(this.status.id, name)
     }
   }
 }
diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue
index dce03f27..cee223e8 100644
--- a/src/components/thread_tree/thread_tree.vue
+++ b/src/components/thread_tree/thread_tree.vue
@@ -22,9 +22,13 @@
       :controlled-showing-tall="currentProp.showingTall"
       :controlled-expanding-subject="currentProp.expandingSubject"
       :controlled-showing-long-subject="currentProp.showingLongSubject"
+      :controlled-replying="currentProp.replying"
+      :controlled-media-playing="currentProp.mediaPlaying"
       :controlled-toggle-showing-tall="() => toggleCurrentProp('showingTall')"
       :controlled-toggle-expanding-subject="() => toggleCurrentProp('expandingSubject')"
       :controlled-toggle-showing-long-subject="() => toggleCurrentProp('showingLongSubject')"
+      :controlled-toggle-replying="() => toggleCurrentProp('replying')"
+      :controlled-set-media-playing="(newVal) => setCurrentProp('mediaPlaying', newVal)"
       :dive="dive ? () => dive(status.id) : undefined"
 
       @goto="setHighlight"
-- 
cgit v1.2.3-70-g09d2


From 9432fcec7ddfce7fd52ee2ba3f0ef531d61d9b46 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sun, 6 Mar 2022 13:50:15 -0500
Subject: Make 'Show full conversation' button have left border in embbeded
 mode

---
 src/components/conversation/conversation.vue | 38 ++++++++++++++--------------
 1 file changed, 19 insertions(+), 19 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 4d64cf08..73c613b9 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -19,29 +19,29 @@
       </button>
     </div>
     <div class="conversation-body panel-body">
-      <div
-        v-if="shouldShowAllConversationButton"
-        class="conversation-dive-to-top-level-box"
-      >
-        <i18n
-          path="status.show_all_conversation_with_icon"
-          tag="button"
-          class="button-unstyled -link"
-          @click.prevent="diveToTopLevel"
-        >
-          <FAIcon
-            place="icon"
-            icon="angle-double-left"
-          />
-          <span place="text">
-            {{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
-          </span>
-        </i18n>
-      </div>
       <div
         v-if="isTreeView"
         class="thread-body"
       >
+        <div
+          v-if="shouldShowAllConversationButton"
+          class="conversation-dive-to-top-level-box"
+        >
+          <i18n
+            path="status.show_all_conversation_with_icon"
+            tag="button"
+            class="button-unstyled -link"
+            @click.prevent="diveToTopLevel"
+          >
+            <FAIcon
+              place="icon"
+              icon="angle-double-left"
+            />
+            <span place="text">
+              {{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
+            </span>
+          </i18n>
+        </div>
         <div
           v-if="shouldShowAncestors"
           class="thread-ancestors"
-- 
cgit v1.2.3-70-g09d2


From d157f6738195325c59870fe02df8fa16985c84af Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sun, 6 Mar 2022 14:13:35 -0500
Subject: Make fading ancestors optional

---
 src/components/conversation/conversation.js        | 3 +++
 src/components/conversation/conversation.vue       | 4 ++--
 src/components/settings_modal/tabs/general_tab.vue | 5 +++++
 src/modules/config.js                              | 1 +
 src/modules/instance.js                            | 1 +
 5 files changed, 12 insertions(+), 2 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 46228e37..89ff2f6f 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -89,6 +89,9 @@ const conversation = {
     isLinearView () {
       return this.displayStyle === 'linear'
     },
+    shouldFadeAncestors () {
+      return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
+    },
     otherRepliesButtonPosition () {
       return this.$store.getters.mergedConfig.conversationOtherRepliesButton
     },
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 73c613b9..3e96fba2 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -50,7 +50,7 @@
             v-for="status in ancestorsOf(diveRoot)"
             :key="status.id"
             class="thread-ancestor"
-            :class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1}"
+            :class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}"
           >
             <status
               ref="statusComponent"
@@ -210,7 +210,7 @@
     border-left: 2px solid var(--border, $fallback--border);
   }
 
-  .thread-ancestor .StatusContent {
+  .thread-ancestor.-faded .StatusContent {
     --link: var(--faintLink);
     --text: var(--faint);
     color: var(--text);
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 28b39d7b..6c108292 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -170,6 +170,11 @@
               {{ $t('settings.tree_advanced') }}
             </BooleanSetting>
           </li>
+          <li>
+            <BooleanSetting path="conversationTreeFadeAncestors">
+              {{ $t('settings.tree_fade_ancestors') }}
+            </BooleanSetting>
+          </li>
           <li>
             <label for="maxDepthInThread">
               {{ $t('settings.max_depth_in_thread') }}
diff --git a/src/modules/config.js b/src/modules/config.js
index 8cb3097c..acc71b80 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -87,6 +87,7 @@ export const defaultState = {
   conversationDisplay: undefined, // instance default
   conversationTreeAdvanced: undefined, // instance default
   conversationOtherRepliesButton: undefined, // instance default
+  conversationTreeFadeAncestors: undefined, // instance default
   maxDepthInThread: 6
 }
 
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 6c5cb4b1..f09dd0b3 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -56,6 +56,7 @@ const defaultState = {
   conversationDisplay: 'linear',
   conversationTreeAdvanced: false,
   conversationOtherRepliesButton: 'below',
+  conversationTreeFadeAncestors: false,
   maxDepthInThread: 6,
 
   // Nasty stuff
-- 
cgit v1.2.3-70-g09d2


From 415a823af09b61e7ca7adfe295ad3a5eebc5e8b5 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Mon, 7 Mar 2022 19:19:12 -0500
Subject: Clean up dead code

---
 src/components/conversation/conversation.js  | 2 --
 src/components/conversation/conversation.vue | 6 ------
 src/components/thread_tree/thread_tree.vue   | 1 -
 3 files changed, 9 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 89ff2f6f..2ef2977a 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -145,8 +145,6 @@ const conversation = {
 
       return sortAndFilterConversation(conversation, this.status)
     },
-    conversationDive () {
-    },
     statusMap () {
       return this.conversation.reduce((res, s) => {
         res[s.id] = s
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 3e96fba2..20dea4f9 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -230,7 +230,6 @@
   }
   .thread-ancestor-dive-box-inner {
     padding: $status-margin;
-    //border-left: 2px solid var(--border, $fallback--border);
   }
 
   .conversation-status {
@@ -263,10 +262,5 @@
     border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
     border-bottom: 1px solid var(--border, $fallback--border);
   }
-  /* &.-expanded { */
-  /*   .conversation-status:last-child { */
-  /*     border-bottom: none; */
-  /*   } */
-  /* } */
 }
 </style>
diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue
index cee223e8..a29a5bef 100644
--- a/src/components/thread_tree/thread_tree.vue
+++ b/src/components/thread_tree/thread_tree.vue
@@ -119,7 +119,6 @@
 
 .thread-tree-replies-hidden {
   padding: $status-margin;
-  //border-top: 1px solid var(--border, $fallback--border);
   /* Make the button stretch along the whole row */
   display: flex;
   align-items: stretch;
-- 
cgit v1.2.3-70-g09d2


From a511250b63b337f4f541a77e57cf6fb7ecbd5d1a Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Mon, 7 Mar 2022 19:28:38 -0500
Subject: Make $status-margin fallback only

---
 src/components/conversation/conversation.vue     |  8 ++++----
 src/components/status/status.scss                | 18 +++++++++---------
 src/components/status_content/status_content.vue |  4 ----
 src/components/thread_tree/thread_tree.vue       |  4 ++--
 4 files changed, 15 insertions(+), 19 deletions(-)

(limited to 'src/components/conversation/conversation.vue')

diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 20dea4f9..7628ceaa 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -194,7 +194,7 @@
 
 .Conversation {
   .conversation-dive-to-top-level-box {
-    padding: $status-margin;
+    padding: var(--status-margin, $status-margin);
     border-bottom-width: 1px;
     border-bottom-style: solid;
     border-bottom-color: var(--border, $fallback--border);
@@ -206,7 +206,7 @@
   }
 
   .thread-ancestors {
-    margin-left: $status-margin;
+    margin-left: var(--status-margin, $status-margin);
     border-left: 2px solid var(--border, $fallback--border);
   }
 
@@ -216,7 +216,7 @@
     color: var(--text);
   }
   .thread-ancestor-dive-box {
-    padding-left: $status-margin;
+    padding-left: var(--status-margin, $status-margin);
     border-bottom-width: 1px;
     border-bottom-style: solid;
     border-bottom-color: var(--border, $fallback--border);
@@ -229,7 +229,7 @@
     }
   }
   .thread-ancestor-dive-box-inner {
-    padding: $status-margin;
+    padding: var(--status-margin, $status-margin);
   }
 
   .conversation-status {
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index 80bc392d..3f647b25 100644
--- a/src/components/status/status.scss
+++ b/src/components/status/status.scss
@@ -27,7 +27,7 @@
   }
 
   .gravestone {
-    padding: $status-margin;
+    padding: var(--status-margin, $status-margin);
     color: $fallback--faint;
     color: var(--faint, $fallback--faint);
     display: flex;
@@ -40,7 +40,7 @@
 
   .status-container {
     display: flex;
-    padding: $status-margin;
+    padding: var(--status-margin, $status-margin);
 
     &.-repeat {
       padding-top: 0;
@@ -48,7 +48,7 @@
   }
 
   .pin {
-    padding: $status-margin $status-margin 0;
+    padding: var(--status-margin, $status-margin) var(--status-margin, $status-margin) 0;
     display: flex;
     align-items: center;
     justify-content: flex-end;
@@ -64,7 +64,7 @@
   }
 
   .left-side {
-    margin-right: $status-margin;
+    margin-right: var(--status-margin, $status-margin);
   }
 
   .right-side {
@@ -73,7 +73,7 @@
   }
 
   .usercard {
-    margin-bottom: $status-margin;
+    margin-bottom: var(--status-margin, $status-margin);
   }
 
   .status-username {
@@ -239,7 +239,7 @@
   }
 
   .repeat-info {
-    padding: 0.4em $status-margin;
+    padding: 0.4em var(--status-margin, $status-margin);
 
     .repeat-icon {
       color: $fallback--cGreen;
@@ -285,7 +285,7 @@
     position: relative;
     width: 100%;
     display: flex;
-    margin-top: $status-margin;
+    margin-top: var(--status-margin, $status-margin);
 
     > * {
       max-width: 4em;
@@ -353,7 +353,7 @@
   }
 
   .favs-repeated-users {
-    margin-top: $status-margin;
+    margin-top: var(--status-margin, $status-margin);
   }
 
   .stats {
@@ -380,7 +380,7 @@
   }
 
   .stat-count {
-    margin-right: $status-margin;
+    margin-right: var(--status-margin, $status-margin);
     user-select: none;
 
     .stat-title {
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 0a09cda4..9e7d7956 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -58,10 +58,6 @@
 
 <script src="./status_content.js" ></script>
 <style lang="scss">
-@import '../../_variables.scss';
-
-$status-margin: 0.75em;
-
 .StatusContent {
   flex: 1;
   min-width: 0;
diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue
index a29a5bef..e64455e0 100644
--- a/src/components/thread_tree/thread_tree.vue
+++ b/src/components/thread_tree/thread_tree.vue
@@ -113,12 +113,12 @@
 <style lang="scss">
 @import '../../_variables.scss';
 .thread-tree-replies {
-  margin-left: $status-margin;
+  margin-left: var(--status-margin, $status-margin);
   border-left: 2px solid var(--border, $fallback--border);
 }
 
 .thread-tree-replies-hidden {
-  padding: $status-margin;
+  padding: var(--status-margin, $status-margin);
   /* Make the button stretch along the whole row */
   display: flex;
   align-items: stretch;
-- 
cgit v1.2.3-70-g09d2

