Add tree-style thread display
This commit is contained in:
parent
7e1e8ea429
commit
0582f19e7c
8 changed files with 224 additions and 21 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
52
src/components/thread_tree/thread_tree.js
Normal file
52
src/components/thread_tree/thread_tree.js
Normal 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
|
55
src/components/thread_tree/thread_tree.vue
Normal file
55
src/components/thread_tree/thread_tree.vue
Normal 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>
|
|
@ -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
|
||||||
|
|
|
@ -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: [],
|
||||||
|
|
Loading…
Reference in a new issue