Add tree-style thread display

This commit is contained in:
Tusooa Zhu 2021-08-06 20:18:27 -04:00
parent 7e1e8ea429
commit 0582f19e7c
No known key found for this signature in database
GPG key ID: 7B467EDE43A08224
8 changed files with 224 additions and 21 deletions

View file

@ -1,5 +1,8 @@
import { reduce, filter, findIndex, clone, get } from 'lodash' import { reduce, filter, findIndex, clone, get } from 'lodash'
import Status from '../status/status.vue' import Status from '../status/status.vue'
import ThreadTree from '../thread_tree/thread_tree.vue'
const debug = console.log
const sortById = (a, b) => { const sortById = (a, b) => {
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
@ -53,6 +56,15 @@ const conversation = {
} }
}, },
computed: { computed: {
displayStyle () {
return this.$store.state.config.conversationDisplay
},
isTreeView () {
return this.displayStyle === 'tree'
},
isLinearView () {
return this.displayStyle === 'linear'
},
hideStatus () { hideStatus () {
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) { if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
return this.virtualHidden && this.$refs.statusComponent[0].suspendable return this.virtualHidden && this.$refs.statusComponent[0].suspendable
@ -90,6 +102,49 @@ const conversation = {
return sortAndFilterConversation(conversation, this.status) return sortAndFilterConversation(conversation, this.status)
}, },
threadTree () {
const reverseLookupTable = this.conversation.reduce((table, status, index) => {
table[status.id] = index
return table
}, {})
const threads = this.conversation.reduce((a, cur) => {
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)
})
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
if (processed[id]) {
return []
}
processed[id] = true
return [{
status: this.conversation[reverseLookupTable[id]],
id,
depth
}, walk(forest, forest[child], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
}).reduce((a, b) => a.concat(b), [])
const linearized = walk(threads.forest, threads.topLevel)
return linearized
},
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 () { replies () {
let i = 1 let i = 1
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
@ -109,7 +164,7 @@ const conversation = {
}, {}) }, {})
}, },
isExpanded () { isExpanded () {
return this.expanded || this.isPage return !!(this.expanded || this.isPage)
}, },
hiddenStyle () { hiddenStyle () {
const height = (this.status && this.status.virtualHeight) || '120px' const height = (this.status && this.status.virtualHeight) || '120px'
@ -117,7 +172,8 @@ const conversation = {
} }
}, },
components: { components: {
Status Status,
ThreadTree
}, },
watch: { watch: {
statusId (newVal, oldVal) { statusId (newVal, oldVal) {

View file

@ -18,6 +18,28 @@
{{ $t('timeline.collapse') }} {{ $t('timeline.collapse') }}
</button> </button>
</div> </div>
<div v-if="isTreeView">
<thread-tree
v-for="status in topLevel"
:key="status.id"
ref="statusComponent"
:status="status"
:in-profile="inProfile"
:conversation="conversation"
:collapsable="collapsable"
:is-expanded="isExpanded"
:pinned-status-ids-object="pinnedStatusIdsObject"
:profile-user-id="profileUserId"
:focused="focused"
:get-replies="getReplies"
:get-highlight="getHighlight"
:set-highlight="setHighlight"
:toggle-expanded="toggleExpanded"
/>
</div>
<div v-if="isLinearView">
<status <status
v-for="status in conversation" v-for="status in conversation"
:key="status.id" :key="status.id"
@ -37,6 +59,7 @@
@toggleExpanded="toggleExpanded" @toggleExpanded="toggleExpanded"
/> />
</div> </div>
</div>
<div <div
v-else v-else
:style="hiddenStyle" :style="hiddenStyle"

View file

@ -20,6 +20,11 @@ const GeneralTab = {
value: mode, value: mode,
label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`) label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`)
})), })),
conversationDisplayOptions: ['tree', 'linear'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.conversation_display_${mode}`)
})),
mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({ mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({
key: mode, key: mode,
value: mode, value: mode,

View file

@ -152,6 +152,15 @@
{{ $t('settings.show_yous') }} {{ $t('settings.show_yous') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<ChoiceSetting
id="conversationDisplay"
path="conversationDisplay"
:options="conversationDisplayOptions"
>
{{ $t('settings.conversation_display') }}
</ChoiceSetting>
</li>
<li> <li>
<ChoiceSetting <ChoiceSetting
id="mentionLinkDisplay" id="mentionLinkDisplay"

View file

@ -0,0 +1,52 @@
import Status from '../status/status.vue'
const debug = console.log
const ThreadTree = {
components: {
Status
},
name: 'ThreadTree',
props: {
depth: Number,
status: Object,
inProfile: Boolean,
conversation: Array,
collapsable: Boolean,
isExpanded: Boolean,
pinnedStatusIdsObject: Object,
profileUserId: String,
focused: Function,
getHighlight: Function,
getReplies: Function,
setHighlight: Function,
toggleExpanded: Function
},
computed: {
reverseLookupTable () {
return this.conversation.reduce((table, status, index) => {
table[status.id] = index
return table
}, {})
},
currentReplies () {
debug('status:', this.status)
debug('getReplies:', this.getReplies(this.status.id))
return this.getReplies(this.status.id).map(({ id }) => this.statusById(id))
},
},
methods: {
statusById (id) {
return this.conversation[this.reverseLookupTable[id]]
},
collapseThread () {
},
showThread () {
},
showAllSubthreads () {
}
}
}
export default ThreadTree

View file

@ -0,0 +1,55 @@
<template>
<div class="thread-tree panel-body">
<status
:key="status.id"
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"
@goto="setHighlight"
@toggleExpanded="toggleExpanded"
/>
<div
v-if="currentReplies.length"
class="thread-tree-replies"
>
<thread-tree
v-for="replyStatus in currentReplies"
:key="replyStatus.id"
ref="childComponent"
:status="replyStatus"
:in-profile="inProfile"
:conversation="conversation"
:collapsable="collapsable"
:is-expanded="isExpanded"
:pinned-status-ids-object="pinnedStatusIdsObject"
:profile-user-id="profileUserId"
:focused="focused"
:get-replies="getReplies"
:get-highlight="getHighlight"
:set-highlight="setHighlight"
:toggle-expanded="toggleExpanded"
class="conversation-status status-fadein panel-body"
/>
</div>
</div>
</template>
<script src="./thread_tree.js"></script>
<style lang="scss">
.thread-tree-replies {
margin-left: 1em;
}
</style>

View file

@ -12,6 +12,7 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0]
export const multiChoiceProperties = [ export const multiChoiceProperties = [
'postContentType', 'postContentType',
'subjectLineBehavior', 'subjectLineBehavior',
'conversationDisplay', // tree | linear
'mentionLinkDisplay' // short | full_for_remote | full 'mentionLinkDisplay' // short | full_for_remote | full
] ]
@ -81,7 +82,8 @@ export const defaultState = {
hidePostStats: undefined, // instance default hidePostStats: undefined, // instance default
hideUserStats: undefined, // instance default hideUserStats: undefined, // instance default
virtualScrolling: undefined, // instance default virtualScrolling: undefined, // instance default
sensitiveByDefault: undefined // instance default sensitiveByDefault: undefined, // instance default
conversationDisplay: undefined // instance default
} }
// caching the instance default properties // caching the instance default properties

View file

@ -53,6 +53,7 @@ const defaultState = {
theme: 'pleroma-dark', theme: 'pleroma-dark',
virtualScrolling: true, virtualScrolling: true,
sensitiveByDefault: false, sensitiveByDefault: false,
conversationDisplay: 'tree',
// Nasty stuff // Nasty stuff
customEmoji: [], customEmoji: [],