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.
This commit is contained in:
Tusooa Zhu 2021-08-10 23:58:27 -04:00
parent 4adffb4835
commit e560fbc935
No known key found for this signature in database
GPG Key ID: 7B467EDE43A08224
2 changed files with 124 additions and 55 deletions

View File

@ -55,7 +55,7 @@ const conversation = {
expanded: false, expanded: false,
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden' threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
statusContentPropertiesObject: {}, statusContentPropertiesObject: {},
diveHistory: [] inlineDivePosition: null
} }
}, },
props: [ props: [
@ -231,7 +231,10 @@ const conversation = {
return this.topLevel return this.topLevel
}, },
diveRoot () { 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 () { diveDepth () {
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0 return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
@ -332,7 +335,6 @@ const conversation = {
this.fetchConversation() this.fetchConversation()
} else { } else {
// if we collapse it, we should reset the dive // if we collapse it, we should reset the dive
this._diven = false
this.undive() this.undive()
} }
}, },
@ -348,19 +350,6 @@ const conversation = {
if (!this.isExpanded) { if (!this.isExpanded) {
return 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 () { fetchConversation () {
if (this.status) { if (this.status) {
@ -449,26 +438,15 @@ const conversation = {
return this.topLevel[0] ? this.topLevel[0].id : undefined return this.topLevel[0] ? this.topLevel[0].id : undefined
}, },
diveIntoStatus (id, preventScroll) { diveIntoStatus (id, preventScroll) {
this.diveHistory = [...this.diveHistory, id] this.tryScrollTo(id)
if (!preventScroll) {
this.goToCurrent()
}
}, },
diveBack () { diveToTopLevel () {
const oldHighlight = this.highlight this.tryScrollTo(this.topLevel[0].id)
this.diveHistory = [...this.diveHistory.slice(0, this.diveHistory.length - 1)]
if (oldHighlight) {
this.tryScrollTo(this.leastVisibleAncestor(oldHighlight))
}
}, },
// only used when we are not on a page
undive () { undive () {
const oldHighlight = this.highlight this.inlineDivePosition = null
this.diveHistory = [] this.setHighlight(this.statusId)
if (oldHighlight) {
this.tryScrollTo(this.leastVisibleAncestor(oldHighlight))
} else {
this.goToCurrent()
}
}, },
tryScrollTo (id) { tryScrollTo (id) {
if (!id) { if (!id) {
@ -477,8 +455,9 @@ const conversation = {
if (this.isPage) { if (this.isPage) {
// set statusId // set statusId
this.$router.push({ name: 'conversation', params: { id } }) this.$router.push({ name: 'conversation', params: { id } })
} else {
this.inlineDivePosition = id
} }
this.setHighlight(id) this.setHighlight(id)
}, },
goToCurrent () { goToCurrent () {
@ -493,10 +472,24 @@ const conversation = {
return undefined return undefined
} }
const { in_reply_to_status_id: parentId } = status const { in_reply_to_status_id: parentId } = status
if (!this.statusMap[parentId]) {
return undefined
}
return parentId return parentId
}, },
parentOrSelf (id) { parentOrSelf (id) {
return this.parentOf(id) || 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
} }
} }
} }

View File

@ -21,34 +21,88 @@
<div class="conversation-body panel-body"> <div class="conversation-body panel-body">
<div <div
v-if="diveMode" v-if="diveMode"
class="conversation-undive-box" class="conversation-dive-to-top-level-box"
> >
<i18n <i18n
path="status.show_all_conversation" path="status.show_all_conversation"
tag="button" tag="button"
class="button-unstyled -link" class="button-unstyled -link"
@click.prevent="undive" @click.prevent="diveToTopLevel"
> >
<FAIcon icon="angle-double-left" /> <FAIcon icon="angle-double-left" />
</i18n> </i18n>
</div> </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 <div
v-if="isTreeView" v-if="isTreeView"
class="thread-body" 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 <thread-tree
v-for="status in showingTopLevel" v-for="status in showingTopLevel"
:key="status.id" :key="status.id"
@ -128,7 +182,7 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.Conversation { .Conversation {
.conversation-undive-box { .conversation-dive-to-top-level-box {
padding: $status-margin; padding: $status-margin;
border-bottom-width: 1px; border-bottom-width: 1px;
border-bottom-style: solid; border-bottom-style: solid;
@ -140,6 +194,27 @@
flex-direction: column; 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* */ /* HACK: we want the border width to scale with the status *below it* */
.conversation-status { .conversation-status {
border-bottom-width: 1px; border-bottom-width: 1px;
@ -148,6 +223,7 @@
border-radius: 0; border-radius: 0;
} }
.thread-ancestor-has-other-replies .conversation-status,
&.-expanded .thread-tree .conversation-status { &.-expanded .thread-tree .conversation-status {
border-bottom: none; border-bottom: none;
} }
@ -162,10 +238,10 @@
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
border-bottom: 1px solid var(--border, $fallback--border); border-bottom: 1px solid var(--border, $fallback--border);
} }
&.-expanded { /* &.-expanded { */
.conversation-status:last-child { /* .conversation-status:last-child { */
border-bottom: none; /* border-bottom: none; */
} /* } */
} /* } */
} }
</style> </style>