Implement thread folding/expanding
This commit is contained in:
parent
0582f19e7c
commit
0f2fd8a352
6 changed files with 180 additions and 14 deletions
|
@ -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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue