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: [PATCH] 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(-)

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>