Implement thread folding/expanding

This commit is contained in:
Tusooa Zhu 2021-08-07 00:33:06 -04:00
parent 0582f19e7c
commit 0f2fd8a352
No known key found for this signature in database
GPG key ID: 7B467EDE43A08224
6 changed files with 180 additions and 14 deletions

View file

@ -38,7 +38,8 @@ const conversation = {
data () { data () {
return { return {
highlight: null, highlight: null,
expanded: false expanded: false,
threadDisplayStatusObject: {} // id => 'showing' | 'hidden'
} }
}, },
props: [ props: [
@ -56,6 +57,9 @@ const conversation = {
} }
}, },
computed: { computed: {
maxDepthToShowByDefault () {
return 4
},
displayStyle () { displayStyle () {
return this.$store.state.config.conversationDisplay return this.$store.state.config.conversationDisplay
}, },
@ -112,15 +116,14 @@ const conversation = {
const id = cur.id const id = cur.id
a.forest[id] = this.getReplies(id) a.forest[id] = this.getReplies(id)
.map(s => s.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 return a
}, { }, {
forest: {}, forest: {},
topLevel: this.conversation.map(s => s.id)
}) })
debug('threads = ', threads)
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => { const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
if (processed[id]) { if (processed[id]) {
return [] return []
@ -131,18 +134,63 @@ const conversation = {
status: this.conversation[reverseLookupTable[id]], status: this.conversation[reverseLookupTable[id]],
id, id,
depth 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), []) }).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 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 () { topLevel () {
const topLevel = this.conversation.reduce((tl, cur) => const topLevel = this.conversation.reduce((tl, cur) =>
tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation) tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation)
debug("toplevel =", topLevel) debug("toplevel =", topLevel)
debug("toplevel =", topLevel)
return topLevel return topLevel
}, },
replies () { replies () {
@ -169,6 +217,25 @@ const conversation = {
hiddenStyle () { hiddenStyle () {
const height = (this.status && this.status.virtualHeight) || '120px' const height = (this.status && this.status.virtualHeight) || '120px'
return this.virtualHidden ? { height } : {} 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: { components: {
@ -235,6 +302,30 @@ const conversation = {
getConversationId (statusId) { getConversationId (statusId) {
const status = this.$store.state.statuses.allStatusesObject[statusId] const status = this.$store.state.statuses.allStatusesObject[statusId]
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id')) 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')
} }
} }
} }

View file

@ -23,6 +23,7 @@
v-for="status in topLevel" v-for="status in topLevel"
:key="status.id" :key="status.id"
ref="statusComponent" ref="statusComponent"
:depth="0"
:status="status" :status="status"
:in-profile="inProfile" :in-profile="inProfile"
@ -37,6 +38,12 @@
:get-highlight="getHighlight" :get-highlight="getHighlight"
:set-highlight="setHighlight" :set-highlight="setHighlight"
:toggle-expanded="toggleExpanded" :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>
<div v-if="isLinearView"> <div v-if="isLinearView">

View file

@ -35,7 +35,9 @@ import {
faStar, faStar,
faEyeSlash, faEyeSlash,
faEye, faEye,
faThumbtack faThumbtack,
faAngleDoubleUp,
faAngleDoubleDown
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
@ -52,7 +54,9 @@ library.add(
faEllipsisH, faEllipsisH,
faEyeSlash, faEyeSlash,
faEye, faEye,
faThumbtack faThumbtack,
faAngleDoubleUp,
faAngleDoubleDown
) )
const Status = { const Status = {
@ -89,7 +93,10 @@ const Status = {
'inlineExpanded', 'inlineExpanded',
'showPinned', 'showPinned',
'inProfile', 'inProfile',
'profileUserId' 'profileUserId',
'controlledThreadDisplayStatus',
'controlledToggleThreadDisplay'
], ],
data () { data () {
return { return {
@ -304,6 +311,12 @@ const Status = {
}, },
isSuspendable () { isSuspendable () {
return !this.replying && this.mediaPlaying.length === 0 return !this.replying && this.mediaPlaying.length === 0
},
inThreadForest () {
return !!this.controlledThreadDisplayStatus
},
threadShowing () {
return this.controlledThreadDisplayStatus === 'showing'
} }
}, },
methods: { methods: {
@ -353,6 +366,9 @@ const Status = {
}, },
setHeadTailLinks (headTailLinks) { setHeadTailLinks (headTailLinks) {
this.headTailLinks = headTailLinks this.headTailLinks = headTailLinks
},
toggleThreadDisplay () {
this.controlledToggleThreadDisplay()
} }
}, },
watch: { watch: {

View file

@ -219,6 +219,19 @@
class="fa-scale-110" class="fa-scale-110"
/> />
</button> </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> </span>
</div> </div>
<div <div

View file

@ -21,7 +21,14 @@ const ThreadTree = {
getHighlight: Function, getHighlight: Function,
getReplies: Function, getReplies: Function,
setHighlight: 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: { computed: {
reverseLookupTable () { reverseLookupTable () {
@ -35,6 +42,9 @@ const ThreadTree = {
debug('getReplies:', this.getReplies(this.status.id)) debug('getReplies:', this.getReplies(this.status.id))
return this.getReplies(this.status.id).map(({ id }) => this.statusById(id)) return this.getReplies(this.status.id).map(({ id }) => this.statusById(id))
}, },
threadShowing () {
return this.threadDisplayStatus[this.status.id] === 'showing'
}
}, },
methods: { methods: {
statusById (id) { statusById (id) {

View file

@ -13,18 +13,23 @@
:replies="getReplies(status.id)" :replies="getReplies(status.id)"
:in-profile="inProfile" :in-profile="inProfile"
:profile-user-id="profileUserId" :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" @goto="setHighlight"
@toggleExpanded="toggleExpanded" @toggleExpanded="toggleExpanded"
/> />
<div <div
v-if="currentReplies.length" v-if="currentReplies.length && threadShowing"
class="thread-tree-replies" class="thread-tree-replies"
> >
<thread-tree <thread-tree
v-for="replyStatus in currentReplies" v-for="replyStatus in currentReplies"
:key="replyStatus.id" :key="replyStatus.id"
ref="childComponent" ref="childComponent"
:depth="depth + 1"
:status="replyStatus" :status="replyStatus"
:in-profile="inProfile" :in-profile="inProfile"
@ -40,16 +45,40 @@
:set-highlight="setHighlight" :set-highlight="setHighlight"
:toggle-expanded="toggleExpanded" :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>
<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> </div>
</template> </template>
<script src="./thread_tree.js"></script> <script src="./thread_tree.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss';
.thread-tree-replies { .thread-tree-replies {
margin-left: 1em; 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> </style>