commit: 0f2fd8a3523e9e2cd1ca6fe287eb7304895f2cba
parent 0582f19e7c2c6f916b427d5ecfbbb571178ce841
Author: Tusooa Zhu <tusooa@kazv.moe>
Date: Sat, 7 Aug 2021 00:33:06 -0400
Implement thread folding/expanding
Diffstat:
6 files changed, 180 insertions(+), 14 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>