forked from AkkomaGang/akkoma-fe
Merge branch 'from/develop/tusooa/tree-threading' into 'develop'
Add the option to display threads as trees See merge request pleroma/pleroma-fe!1407
This commit is contained in:
commit
e34d71fc1f
21 changed files with 1168 additions and 95 deletions
|
@ -30,3 +30,5 @@ $fallback--attachmentRadius: 10px;
|
||||||
$fallback--chatMessageRadius: 10px;
|
$fallback--chatMessageRadius: 10px;
|
||||||
|
|
||||||
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||||
|
|
||||||
|
$status-margin: 0.75em;
|
||||||
|
|
|
@ -1,5 +1,19 @@
|
||||||
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'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faAngleDoubleDown,
|
||||||
|
faAngleDoubleLeft,
|
||||||
|
faChevronLeft
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faAngleDoubleDown,
|
||||||
|
faAngleDoubleLeft,
|
||||||
|
faChevronLeft
|
||||||
|
)
|
||||||
|
|
||||||
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
|
||||||
|
@ -35,7 +49,10 @@ const conversation = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
highlight: null,
|
highlight: null,
|
||||||
expanded: false
|
expanded: false,
|
||||||
|
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
|
||||||
|
statusContentPropertiesObject: {},
|
||||||
|
inlineDivePosition: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
|
@ -53,12 +70,50 @@ const conversation = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hideStatus () {
|
maxDepthToShowByDefault () {
|
||||||
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
|
// maxDepthInThread = max number of depths that is *visible*
|
||||||
return this.virtualHidden && this.$refs.statusComponent[0].suspendable
|
// since our depth starts with 0 and "showing" means "showing children"
|
||||||
} else {
|
// there is a -2 here
|
||||||
return this.virtualHidden
|
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
|
||||||
|
return maxDepth >= 1 ? maxDepth : 1
|
||||||
|
},
|
||||||
|
displayStyle () {
|
||||||
|
return this.$store.getters.mergedConfig.conversationDisplay
|
||||||
|
},
|
||||||
|
isTreeView () {
|
||||||
|
return !this.isLinearView
|
||||||
|
},
|
||||||
|
treeViewIsSimple () {
|
||||||
|
return !this.$store.getters.mergedConfig.conversationTreeAdvanced
|
||||||
|
},
|
||||||
|
isLinearView () {
|
||||||
|
return this.displayStyle === 'linear'
|
||||||
|
},
|
||||||
|
shouldFadeAncestors () {
|
||||||
|
return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
|
||||||
|
},
|
||||||
|
otherRepliesButtonPosition () {
|
||||||
|
return this.$store.getters.mergedConfig.conversationOtherRepliesButton
|
||||||
|
},
|
||||||
|
showOtherRepliesButtonBelowStatus () {
|
||||||
|
return this.otherRepliesButtonPosition === 'below'
|
||||||
|
},
|
||||||
|
showOtherRepliesButtonInsideStatus () {
|
||||||
|
return this.otherRepliesButtonPosition === 'inside'
|
||||||
|
},
|
||||||
|
suspendable () {
|
||||||
|
if (this.isTreeView) {
|
||||||
|
return Object.entries(this.statusContentProperties)
|
||||||
|
.every(([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0)
|
||||||
}
|
}
|
||||||
|
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
|
||||||
|
return this.$refs.statusComponent.every(s => s.suspendable)
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hideStatus () {
|
||||||
|
return this.virtualHidden && this.suspendable
|
||||||
},
|
},
|
||||||
status () {
|
status () {
|
||||||
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
||||||
|
@ -90,6 +145,121 @@ const conversation = {
|
||||||
|
|
||||||
return sortAndFilterConversation(conversation, this.status)
|
return sortAndFilterConversation(conversation, this.status)
|
||||||
},
|
},
|
||||||
|
statusMap () {
|
||||||
|
return this.conversation.reduce((res, s) => {
|
||||||
|
res[s.id] = s
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
|
||||||
|
return a
|
||||||
|
}, {
|
||||||
|
forest: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
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[id], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
|
||||||
|
}).reduce((a, b) => a.concat(b), [])
|
||||||
|
|
||||||
|
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 () {
|
||||||
|
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)
|
||||||
|
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 () {
|
||||||
|
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)
|
||||||
|
return topLevel
|
||||||
|
},
|
||||||
|
otherTopLevelCount () {
|
||||||
|
return this.topLevel.length - 1
|
||||||
|
},
|
||||||
|
showingTopLevel () {
|
||||||
|
if (this.canDive && this.diveRoot) {
|
||||||
|
return [this.statusMap[this.diveRoot]]
|
||||||
|
}
|
||||||
|
return this.topLevel
|
||||||
|
},
|
||||||
|
diveRoot () {
|
||||||
|
const statusId = this.inlineDivePosition || this.statusId
|
||||||
|
const isTopLevel = !this.parentOf(statusId)
|
||||||
|
return isTopLevel ? null : statusId
|
||||||
|
},
|
||||||
|
diveDepth () {
|
||||||
|
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
|
||||||
|
},
|
||||||
|
diveMode () {
|
||||||
|
return this.canDive && !!this.diveRoot
|
||||||
|
},
|
||||||
|
shouldShowAllConversationButton () {
|
||||||
|
// The "show all conversation" button tells the user that there exist
|
||||||
|
// other toplevel statuses, so do not show it if there is only a single root
|
||||||
|
return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1
|
||||||
|
},
|
||||||
|
shouldShowAncestors () {
|
||||||
|
return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length
|
||||||
|
},
|
||||||
replies () {
|
replies () {
|
||||||
let i = 1
|
let i = 1
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
|
@ -109,15 +279,71 @@ 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'
|
||||||
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.diveDepth) <= this.maxDepthToShowByDefault) {
|
||||||
|
return 'showing'
|
||||||
|
} else {
|
||||||
|
return 'hidden'
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
a[id] = status
|
||||||
|
return a
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
statusContentProperties () {
|
||||||
|
return this.conversation.reduce((a, k) => {
|
||||||
|
const id = k.id
|
||||||
|
const props = (() => {
|
||||||
|
const def = {
|
||||||
|
showingTall: false,
|
||||||
|
expandingSubject: false,
|
||||||
|
showingLongSubject: false,
|
||||||
|
isReplying: false,
|
||||||
|
mediaPlaying: []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.statusContentPropertiesObject[id]) {
|
||||||
|
return {
|
||||||
|
...def,
|
||||||
|
...this.statusContentPropertiesObject[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
})()
|
||||||
|
|
||||||
|
a[id] = props
|
||||||
|
return a
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
canDive () {
|
||||||
|
return this.isTreeView && this.isExpanded
|
||||||
|
},
|
||||||
|
focused () {
|
||||||
|
return (id) => {
|
||||||
|
return (this.isExpanded) && id === this.highlight
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maybeHighlight () {
|
||||||
|
return this.isExpanded ? this.highlight : null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Status
|
Status,
|
||||||
|
ThreadTree
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
statusId (newVal, oldVal) {
|
statusId (newVal, oldVal) {
|
||||||
|
@ -132,6 +358,8 @@ const conversation = {
|
||||||
expanded (value) {
|
expanded (value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
this.fetchConversation()
|
this.fetchConversation()
|
||||||
|
} else {
|
||||||
|
this.resetDisplayState()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
virtualHidden (value) {
|
virtualHidden (value) {
|
||||||
|
@ -161,8 +389,8 @@ const conversation = {
|
||||||
getReplies (id) {
|
getReplies (id) {
|
||||||
return this.replies[id] || []
|
return this.replies[id] || []
|
||||||
},
|
},
|
||||||
focused (id) {
|
getHighlight () {
|
||||||
return (this.isExpanded) && id === this.statusId
|
return this.isExpanded ? this.highlight : null
|
||||||
},
|
},
|
||||||
setHighlight (id) {
|
setHighlight (id) {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
@ -170,15 +398,139 @@ const conversation = {
|
||||||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||||
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
||||||
},
|
},
|
||||||
getHighlight () {
|
|
||||||
return this.isExpanded ? this.highlight : null
|
|
||||||
},
|
|
||||||
toggleExpanded () {
|
toggleExpanded () {
|
||||||
this.expanded = !this.expanded
|
this.expanded = !this.expanded
|
||||||
},
|
},
|
||||||
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 curStatus = this.threadDisplayStatus[id]
|
||||||
|
const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
|
||||||
|
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')
|
||||||
|
},
|
||||||
|
setStatusContentProperty (id, name, value) {
|
||||||
|
this.statusContentPropertiesObject = {
|
||||||
|
...this.statusContentPropertiesObject,
|
||||||
|
[id]: {
|
||||||
|
...this.statusContentPropertiesObject[id],
|
||||||
|
[name]: value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleStatusContentProperty (id, name) {
|
||||||
|
this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
|
||||||
|
},
|
||||||
|
leastVisibleAncestor (id) {
|
||||||
|
let cur = id
|
||||||
|
let parent = this.parentOf(cur)
|
||||||
|
while (cur) {
|
||||||
|
// if the parent is showing it means cur is visible
|
||||||
|
if (this.threadDisplayStatus[parent] === 'showing') {
|
||||||
|
return cur
|
||||||
|
}
|
||||||
|
parent = this.parentOf(parent)
|
||||||
|
cur = this.parentOf(cur)
|
||||||
|
}
|
||||||
|
// nothing found, fall back to toplevel
|
||||||
|
return this.topLevel[0] ? this.topLevel[0].id : undefined
|
||||||
|
},
|
||||||
|
diveIntoStatus (id, preventScroll) {
|
||||||
|
this.tryScrollTo(id)
|
||||||
|
},
|
||||||
|
diveToTopLevel () {
|
||||||
|
this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id)
|
||||||
|
},
|
||||||
|
// only used when we are not on a page
|
||||||
|
undive () {
|
||||||
|
this.inlineDivePosition = null
|
||||||
|
this.setHighlight(this.statusId)
|
||||||
|
},
|
||||||
|
tryScrollTo (id) {
|
||||||
|
if (!id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.isPage) {
|
||||||
|
// set statusId
|
||||||
|
this.$router.push({ name: 'conversation', params: { id } })
|
||||||
|
} else {
|
||||||
|
this.inlineDivePosition = id
|
||||||
|
}
|
||||||
|
// Because the conversation can be unmounted when out of sight
|
||||||
|
// and mounted again when it comes into sight,
|
||||||
|
// the `mounted` or `created` function in `status` should not
|
||||||
|
// contain scrolling calls, as we do not want the page to jump
|
||||||
|
// when we scroll with an expanded conversation.
|
||||||
|
//
|
||||||
|
// Now the method is to rely solely on the `highlight` watcher
|
||||||
|
// in `status` components.
|
||||||
|
// In linear views, all statuses are rendered at all times, but
|
||||||
|
// in tree views, it is possible that a change in active status
|
||||||
|
// removes and adds status components (e.g. an originally child
|
||||||
|
// status becomes an ancestor status, and thus they will be
|
||||||
|
// different).
|
||||||
|
// Here, let the components be rendered first, in order to trigger
|
||||||
|
// the `highlight` watcher.
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.setHighlight(id)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goToCurrent () {
|
||||||
|
this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
|
||||||
|
},
|
||||||
|
statusById (id) {
|
||||||
|
return this.statusMap[id]
|
||||||
|
},
|
||||||
|
parentOf (id) {
|
||||||
|
const status = this.statusById(id)
|
||||||
|
if (!status) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const { in_reply_to_status_id: parentId } = status
|
||||||
|
if (!this.statusMap[parentId]) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return parentId
|
||||||
|
},
|
||||||
|
parentOrSelf (id) {
|
||||||
|
return this.parentOf(id) || id
|
||||||
|
},
|
||||||
|
// Ancestors of some status, from top to bottom
|
||||||
|
ancestorsOf (id) {
|
||||||
|
const ancestors = []
|
||||||
|
let cur = this.parentOf(id)
|
||||||
|
while (cur) {
|
||||||
|
ancestors.unshift(this.statusMap[cur])
|
||||||
|
cur = this.parentOf(cur)
|
||||||
|
}
|
||||||
|
return ancestors
|
||||||
|
},
|
||||||
|
topLevelAncestorOrSelfId (id) {
|
||||||
|
let cur = id
|
||||||
|
let parent = this.parentOf(id)
|
||||||
|
while (parent) {
|
||||||
|
cur = this.parentOf(cur)
|
||||||
|
parent = this.parentOf(parent)
|
||||||
|
}
|
||||||
|
return cur
|
||||||
|
},
|
||||||
|
resetDisplayState () {
|
||||||
|
this.undive()
|
||||||
|
this.threadDisplayStatusObject = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,24 +18,168 @@
|
||||||
{{ $t('timeline.collapse') }}
|
{{ $t('timeline.collapse') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<status
|
<div class="conversation-body panel-body">
|
||||||
v-for="status in conversation"
|
<div
|
||||||
:key="status.id"
|
v-if="isTreeView"
|
||||||
ref="statusComponent"
|
class="thread-body"
|
||||||
:inline-expanded="collapsable && isExpanded"
|
>
|
||||||
:statusoid="status"
|
<div
|
||||||
:expandable="!isExpanded"
|
v-if="shouldShowAllConversationButton"
|
||||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
class="conversation-dive-to-top-level-box"
|
||||||
:focused="focused(status.id)"
|
>
|
||||||
:in-conversation="isExpanded"
|
<i18n
|
||||||
:highlight="getHighlight()"
|
path="status.show_all_conversation_with_icon"
|
||||||
:replies="getReplies(status.id)"
|
tag="button"
|
||||||
:in-profile="inProfile"
|
class="button-unstyled -link"
|
||||||
:profile-user-id="profileUserId"
|
@click.prevent="diveToTopLevel"
|
||||||
class="conversation-status status-fadein panel-body"
|
>
|
||||||
@goto="setHighlight"
|
<FAIcon
|
||||||
@toggleExpanded="toggleExpanded"
|
place="icon"
|
||||||
/>
|
icon="angle-double-left"
|
||||||
|
/>
|
||||||
|
<span place="text">
|
||||||
|
{{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
|
||||||
|
</span>
|
||||||
|
</i18n>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="shouldShowAncestors"
|
||||||
|
class="thread-ancestors"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="status in ancestorsOf(diveRoot)"
|
||||||
|
:key="status.id"
|
||||||
|
class="thread-ancestor"
|
||||||
|
:class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}"
|
||||||
|
>
|
||||||
|
<status
|
||||||
|
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"
|
||||||
|
|
||||||
|
:simple-tree="treeViewIsSimple"
|
||||||
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
|
:thread-display-status="threadDisplayStatus"
|
||||||
|
:show-thread-recursively="showThreadRecursively"
|
||||||
|
:total-reply-count="totalReplyCount"
|
||||||
|
:total-reply-depth="totalReplyDepth"
|
||||||
|
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
||||||
|
:dive="() => diveIntoStatus(status.id)"
|
||||||
|
|
||||||
|
:controlled-showing-tall="statusContentProperties[status.id].showingTall"
|
||||||
|
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
|
||||||
|
:controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject"
|
||||||
|
:controlled-replying="statusContentProperties[status.id].replying"
|
||||||
|
:controlled-media-playing="statusContentProperties[status.id].mediaPlaying"
|
||||||
|
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
|
||||||
|
:controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')"
|
||||||
|
:controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')"
|
||||||
|
:controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')"
|
||||||
|
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
|
||||||
|
|
||||||
|
@goto="setHighlight"
|
||||||
|
@toggleExpanded="toggleExpanded"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
||||||
|
class="thread-ancestor-dive-box"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="thread-ancestor-dive-box-inner"
|
||||||
|
>
|
||||||
|
<i18n
|
||||||
|
tag="button"
|
||||||
|
path="status.ancestor_follow_with_icon"
|
||||||
|
class="button-unstyled -link thread-tree-show-replies-button"
|
||||||
|
@click.prevent="diveIntoStatus(status.id)"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
place="icon"
|
||||||
|
icon="angle-double-right"
|
||||||
|
/>
|
||||||
|
<span place="text">
|
||||||
|
{{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }}
|
||||||
|
</span>
|
||||||
|
</i18n>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<thread-tree
|
||||||
|
v-for="status in showingTopLevel"
|
||||||
|
:key="status.id"
|
||||||
|
ref="statusComponent"
|
||||||
|
:depth="0"
|
||||||
|
|
||||||
|
: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"
|
||||||
|
:highlight="maybeHighlight"
|
||||||
|
:set-highlight="setHighlight"
|
||||||
|
:toggle-expanded="toggleExpanded"
|
||||||
|
|
||||||
|
:simple="treeViewIsSimple"
|
||||||
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
|
:thread-display-status="threadDisplayStatus"
|
||||||
|
:show-thread-recursively="showThreadRecursively"
|
||||||
|
:total-reply-count="totalReplyCount"
|
||||||
|
:total-reply-depth="totalReplyDepth"
|
||||||
|
:status-content-properties="statusContentProperties"
|
||||||
|
:set-status-content-property="setStatusContentProperty"
|
||||||
|
:toggle-status-content-property="toggleStatusContentProperty"
|
||||||
|
:dive="canDive ? diveIntoStatus : undefined"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isLinearView"
|
||||||
|
class="thread-body"
|
||||||
|
>
|
||||||
|
<status
|
||||||
|
v-for="status in conversation"
|
||||||
|
: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"
|
||||||
|
|
||||||
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
|
:thread-display-status="threadDisplayStatus"
|
||||||
|
:show-thread-recursively="showThreadRecursively"
|
||||||
|
:total-reply-count="totalReplyCount"
|
||||||
|
:total-reply-depth="totalReplyDepth"
|
||||||
|
:status-content-properties="statusContentProperties"
|
||||||
|
:set-status-content-property="setStatusContentProperty"
|
||||||
|
:toggle-status-content-property="toggleStatusContentProperty"
|
||||||
|
|
||||||
|
@goto="setHighlight"
|
||||||
|
@toggleExpanded="toggleExpanded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
|
@ -49,6 +193,45 @@
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.Conversation {
|
.Conversation {
|
||||||
|
.conversation-dive-to-top-level-box {
|
||||||
|
padding: var(--status-margin, $status-margin);
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-color: var(--border, $fallback--border);
|
||||||
|
border-radius: 0;
|
||||||
|
/* Make the button stretch along the whole row */
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-ancestors {
|
||||||
|
margin-left: var(--status-margin, $status-margin);
|
||||||
|
border-left: 2px solid var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-ancestor.-faded .StatusContent {
|
||||||
|
--link: var(--faintLink);
|
||||||
|
--text: var(--faint);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.thread-ancestor-dive-box {
|
||||||
|
padding-left: var(--status-margin, $status-margin);
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-color: var(--border, $fallback--border);
|
||||||
|
border-radius: 0;
|
||||||
|
/* Make the button stretch along the whole row */
|
||||||
|
&, &-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.thread-ancestor-dive-box-inner {
|
||||||
|
padding: var(--status-margin, $status-margin);
|
||||||
|
}
|
||||||
|
|
||||||
.conversation-status {
|
.conversation-status {
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
border-bottom-style: solid;
|
border-bottom-style: solid;
|
||||||
|
@ -56,12 +239,28 @@
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-expanded {
|
.thread-ancestor-has-other-replies .conversation-status,
|
||||||
.conversation-status:last-child {
|
.thread-ancestor:last-child .conversation-status,
|
||||||
border-bottom: none;
|
.thread-ancestor:last-child .thread-ancestor-dive-box,
|
||||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
&.-expanded .thread-tree .conversation-status {
|
||||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thread-ancestors + .thread-tree > .conversation-status {
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-top-style: solid;
|
||||||
|
border-top-color: var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* expanded conversation in timeline */
|
||||||
|
&.status-fadein.-expanded .thread-body {
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-left-style: solid;
|
||||||
|
border-left-color: $fallback--cRed;
|
||||||
|
border-left-color: var(--cRed, $fallback--cRed);
|
||||||
|
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||||
|
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||||
|
border-bottom: 1px solid var(--border, $fallback--border);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
37
src/components/settings_modal/helpers/integer_setting.js
Normal file
37
src/components/settings_modal/helpers/integer_setting.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { get, set } from 'lodash'
|
||||||
|
import ModifiedIndicator from './modified_indicator.vue'
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ModifiedIndicator
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
path: String,
|
||||||
|
disabled: Boolean,
|
||||||
|
min: Number
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
pathDefault () {
|
||||||
|
const [firstSegment, ...rest] = this.path.split('.')
|
||||||
|
return [firstSegment + 'DefaultValue', ...rest].join('.')
|
||||||
|
},
|
||||||
|
state () {
|
||||||
|
const value = get(this.$parent, this.path)
|
||||||
|
if (value === undefined) {
|
||||||
|
return this.defaultState
|
||||||
|
} else {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultState () {
|
||||||
|
return get(this.$parent, this.pathDefault)
|
||||||
|
},
|
||||||
|
isChanged () {
|
||||||
|
return this.state !== this.defaultState
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
update (e) {
|
||||||
|
set(this.$parent, this.path, parseInt(e.target.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
src/components/settings_modal/helpers/integer_setting.vue
Normal file
20
src/components/settings_modal/helpers/integer_setting.vue
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<span class="IntegerSetting">
|
||||||
|
<label :for="path">
|
||||||
|
<slot />
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
:id="path"
|
||||||
|
class="number-input"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
:disabled="disabled"
|
||||||
|
:min="min || 0"
|
||||||
|
:value="state"
|
||||||
|
@change="update"
|
||||||
|
>
|
||||||
|
<ModifiedIndicator :changed="isChanged" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./integer_setting.js"></script>
|
|
@ -1,6 +1,7 @@
|
||||||
import { filter, trim } from 'lodash'
|
import { filter, trim } from 'lodash'
|
||||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||||
|
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||||
|
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
|
|
||||||
|
@ -17,7 +18,8 @@ const FilteringTab = {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
BooleanSetting,
|
BooleanSetting,
|
||||||
ChoiceSetting
|
ChoiceSetting,
|
||||||
|
IntegerSetting
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...SharedComputedObject(),
|
...SharedComputedObject(),
|
||||||
|
|
|
@ -70,17 +70,12 @@
|
||||||
</li>
|
</li>
|
||||||
<h3>{{ $t('settings.attachments') }}</h3>
|
<h3>{{ $t('settings.attachments') }}</h3>
|
||||||
<li>
|
<li>
|
||||||
<label for="maxThumbnails">
|
<IntegerSetting
|
||||||
{{ $t('settings.max_thumbnails') }}
|
path="maxThumbnails"
|
||||||
</label>
|
:min="0"
|
||||||
<input
|
|
||||||
id="maxThumbnails"
|
|
||||||
path.number="maxThumbnails"
|
|
||||||
class="number-input"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
>
|
>
|
||||||
|
{{ $t('settings.max_thumbnails') }}
|
||||||
|
</IntegerSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="hideAttachments">
|
<BooleanSetting path="hideAttachments">
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||||
|
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||||
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
||||||
|
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
|
@ -20,6 +21,16 @@ 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}`)
|
||||||
|
})),
|
||||||
|
conversationOtherRepliesButtonOptions: ['below', 'inside'].map(mode => ({
|
||||||
|
key: mode,
|
||||||
|
value: mode,
|
||||||
|
label: this.$t(`settings.conversation_other_replies_button_${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,
|
||||||
|
@ -37,6 +48,7 @@ const GeneralTab = {
|
||||||
components: {
|
components: {
|
||||||
BooleanSetting,
|
BooleanSetting,
|
||||||
ChoiceSetting,
|
ChoiceSetting,
|
||||||
|
IntegerSetting,
|
||||||
InterfaceLanguageSwitcher
|
InterfaceLanguageSwitcher
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -152,6 +152,47 @@
|
||||||
{{ $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>
|
||||||
|
<ul
|
||||||
|
v-if="conversationDisplay !== 'linear'"
|
||||||
|
class="setting-list suboptions"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="conversationTreeAdvanced">
|
||||||
|
{{ $t('settings.tree_advanced') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="conversationTreeFadeAncestors">
|
||||||
|
{{ $t('settings.tree_fade_ancestors') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
path="maxDepthInThread"
|
||||||
|
:min="3"
|
||||||
|
>
|
||||||
|
{{ $t('settings.max_depth_in_thread') }}
|
||||||
|
</IntegerSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ChoiceSetting
|
||||||
|
id="conversationOtherRepliesButton"
|
||||||
|
path="conversationOtherRepliesButton"
|
||||||
|
:options="conversationOtherRepliesButtonOptions"
|
||||||
|
>
|
||||||
|
{{ $t('settings.conversation_other_replies_button') }}
|
||||||
|
</ChoiceSetting>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
<li>
|
<li>
|
||||||
<ChoiceSetting
|
<ChoiceSetting
|
||||||
id="mentionLinkDisplay"
|
id="mentionLinkDisplay"
|
||||||
|
|
|
@ -35,7 +35,10 @@ import {
|
||||||
faStar,
|
faStar,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faEye,
|
faEye,
|
||||||
faThumbtack
|
faThumbtack,
|
||||||
|
faChevronUp,
|
||||||
|
faChevronDown,
|
||||||
|
faAngleDoubleRight
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
|
@ -52,9 +55,47 @@ library.add(
|
||||||
faEllipsisH,
|
faEllipsisH,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faEye,
|
faEye,
|
||||||
faThumbtack
|
faThumbtack,
|
||||||
|
faChevronUp,
|
||||||
|
faChevronDown,
|
||||||
|
faAngleDoubleRight
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const camelCase = name => name.charAt(0).toUpperCase() + name.slice(1)
|
||||||
|
|
||||||
|
const controlledOrUncontrolledGetters = list => list.reduce((res, name) => {
|
||||||
|
const camelized = camelCase(name)
|
||||||
|
const toggle = `controlledToggle${camelized}`
|
||||||
|
const controlledName = `controlled${camelized}`
|
||||||
|
const uncontrolledName = `uncontrolled${camelized}`
|
||||||
|
res[name] = function () {
|
||||||
|
return this[toggle] ? this[controlledName] : this[uncontrolledName]
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const controlledOrUncontrolledToggle = (obj, name) => {
|
||||||
|
const camelized = camelCase(name)
|
||||||
|
const toggle = `controlledToggle${camelized}`
|
||||||
|
const uncontrolledName = `uncontrolled${camelized}`
|
||||||
|
if (obj[toggle]) {
|
||||||
|
obj[toggle]()
|
||||||
|
} else {
|
||||||
|
obj[uncontrolledName] = !obj[uncontrolledName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const controlledOrUncontrolledSet = (obj, name, val) => {
|
||||||
|
const camelized = camelCase(name)
|
||||||
|
const set = `controlledSet${camelized}`
|
||||||
|
const uncontrolledName = `uncontrolled${camelized}`
|
||||||
|
if (obj[set]) {
|
||||||
|
obj[set](val)
|
||||||
|
} else {
|
||||||
|
obj[uncontrolledName] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const Status = {
|
const Status = {
|
||||||
name: 'Status',
|
name: 'Status',
|
||||||
components: {
|
components: {
|
||||||
|
@ -89,20 +130,38 @@ const Status = {
|
||||||
'inlineExpanded',
|
'inlineExpanded',
|
||||||
'showPinned',
|
'showPinned',
|
||||||
'inProfile',
|
'inProfile',
|
||||||
'profileUserId'
|
'profileUserId',
|
||||||
|
|
||||||
|
'simpleTree',
|
||||||
|
'controlledThreadDisplayStatus',
|
||||||
|
'controlledToggleThreadDisplay',
|
||||||
|
'showOtherRepliesAsButton',
|
||||||
|
|
||||||
|
'controlledShowingTall',
|
||||||
|
'controlledToggleShowingTall',
|
||||||
|
'controlledExpandingSubject',
|
||||||
|
'controlledToggleExpandingSubject',
|
||||||
|
'controlledShowingLongSubject',
|
||||||
|
'controlledToggleShowingLongSubject',
|
||||||
|
'controlledReplying',
|
||||||
|
'controlledToggleReplying',
|
||||||
|
'controlledMediaPlaying',
|
||||||
|
'controlledSetMediaPlaying',
|
||||||
|
'dive'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
replying: false,
|
uncontrolledReplying: false,
|
||||||
unmuted: false,
|
unmuted: false,
|
||||||
userExpanded: false,
|
userExpanded: false,
|
||||||
mediaPlaying: [],
|
uncontrolledMediaPlaying: [],
|
||||||
suspendable: true,
|
suspendable: true,
|
||||||
error: null,
|
error: null,
|
||||||
headTailLinks: null
|
headTailLinks: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...controlledOrUncontrolledGetters(['replying', 'mediaPlaying']),
|
||||||
muteWords () {
|
muteWords () {
|
||||||
return this.mergedConfig.muteWords
|
return this.mergedConfig.muteWords
|
||||||
},
|
},
|
||||||
|
@ -318,6 +377,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: {
|
||||||
|
@ -340,7 +405,7 @@ const Status = {
|
||||||
this.error = undefined
|
this.error = undefined
|
||||||
},
|
},
|
||||||
toggleReplying () {
|
toggleReplying () {
|
||||||
this.replying = !this.replying
|
controlledOrUncontrolledToggle(this, 'replying')
|
||||||
},
|
},
|
||||||
gotoOriginal (id) {
|
gotoOriginal (id) {
|
||||||
if (this.inConversation) {
|
if (this.inConversation) {
|
||||||
|
@ -360,17 +425,19 @@ const Status = {
|
||||||
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||||
},
|
},
|
||||||
addMediaPlaying (id) {
|
addMediaPlaying (id) {
|
||||||
this.mediaPlaying.push(id)
|
controlledOrUncontrolledSet(this, 'mediaPlaying', this.mediaPlaying.concat(id))
|
||||||
},
|
},
|
||||||
removeMediaPlaying (id) {
|
removeMediaPlaying (id) {
|
||||||
this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id)
|
controlledOrUncontrolledSet(this, 'mediaPlaying', this.mediaPlaying.filter(mediaId => mediaId !== id))
|
||||||
},
|
},
|
||||||
setHeadTailLinks (headTailLinks) {
|
setHeadTailLinks (headTailLinks) {
|
||||||
this.headTailLinks = headTailLinks
|
this.headTailLinks = headTailLinks
|
||||||
}
|
},
|
||||||
},
|
toggleThreadDisplay () {
|
||||||
watch: {
|
this.controlledToggleThreadDisplay()
|
||||||
'highlight': function (id) {
|
},
|
||||||
|
scrollIfHighlighted (highlightId) {
|
||||||
|
const id = highlightId
|
||||||
if (this.status.id === id) {
|
if (this.status.id === id) {
|
||||||
let rect = this.$el.getBoundingClientRect()
|
let rect = this.$el.getBoundingClientRect()
|
||||||
if (rect.top < 100) {
|
if (rect.top < 100) {
|
||||||
|
@ -384,6 +451,11 @@ const Status = {
|
||||||
window.scrollBy(0, rect.bottom - window.innerHeight + 50)
|
window.scrollBy(0, rect.bottom - window.innerHeight + 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'highlight': function (id) {
|
||||||
|
this.scrollIfHighlighted(id)
|
||||||
},
|
},
|
||||||
'status.repeat_num': function (num) {
|
'status.repeat_num': function (num) {
|
||||||
// refetch repeats when repeat_num is changed in any way
|
// refetch repeats when repeat_num is changed in any way
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
$status-margin: 0.75em;
|
|
||||||
|
|
||||||
.Status {
|
.Status {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
@ -28,15 +26,8 @@ $status-margin: 0.75em;
|
||||||
--icon: var(--selectedPostIcon, $fallback--icon);
|
--icon: var(--selectedPostIcon, $fallback--icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-conversation {
|
|
||||||
border-left-width: 4px;
|
|
||||||
border-left-style: solid;
|
|
||||||
border-left-color: $fallback--cRed;
|
|
||||||
border-left-color: var(--cRed, $fallback--cRed);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gravestone {
|
.gravestone {
|
||||||
padding: $status-margin;
|
padding: var(--status-margin, $status-margin);
|
||||||
color: $fallback--faint;
|
color: $fallback--faint;
|
||||||
color: var(--faint, $fallback--faint);
|
color: var(--faint, $fallback--faint);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -49,7 +40,7 @@ $status-margin: 0.75em;
|
||||||
|
|
||||||
.status-container {
|
.status-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: $status-margin;
|
padding: var(--status-margin, $status-margin);
|
||||||
|
|
||||||
&.-repeat {
|
&.-repeat {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
@ -57,7 +48,7 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pin {
|
.pin {
|
||||||
padding: $status-margin $status-margin 0;
|
padding: var(--status-margin, $status-margin) var(--status-margin, $status-margin) 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
@ -73,7 +64,7 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-side {
|
.left-side {
|
||||||
margin-right: $status-margin;
|
margin-right: var(--status-margin, $status-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-side {
|
.right-side {
|
||||||
|
@ -82,7 +73,7 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usercard {
|
.usercard {
|
||||||
margin-bottom: $status-margin;
|
margin-bottom: var(--status-margin, $status-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-username {
|
.status-username {
|
||||||
|
@ -248,7 +239,7 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repeat-info {
|
.repeat-info {
|
||||||
padding: 0.4em $status-margin;
|
padding: 0.4em var(--status-margin, $status-margin);
|
||||||
|
|
||||||
.repeat-icon {
|
.repeat-icon {
|
||||||
color: $fallback--cGreen;
|
color: $fallback--cGreen;
|
||||||
|
@ -294,7 +285,7 @@ $status-margin: 0.75em;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: $status-margin;
|
margin-top: var(--status-margin, $status-margin);
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
max-width: 4em;
|
max-width: 4em;
|
||||||
|
@ -362,7 +353,7 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.favs-repeated-users {
|
.favs-repeated-users {
|
||||||
margin-top: $status-margin;
|
margin-top: var(--status-margin, $status-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats {
|
.stats {
|
||||||
|
@ -389,7 +380,7 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-count {
|
.stat-count {
|
||||||
margin-right: $status-margin;
|
margin-right: var(--status-margin, $status-margin);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
.stat-title {
|
.stat-title {
|
||||||
|
|
|
@ -221,6 +221,31 @@
|
||||||
class="fa-scale-110"
|
class="fa-scale-110"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="inThreadForest && replies && replies.length && !simpleTree"
|
||||||
|
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 ? 'chevron-up' : 'chevron-down'"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="dive && !simpleTree"
|
||||||
|
class="button-unstyled"
|
||||||
|
:title="$t('status.show_only_conversation_under_this')"
|
||||||
|
@click.prevent="dive"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110"
|
||||||
|
:icon="'angle-double-right'"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -308,6 +333,12 @@
|
||||||
:no-heading="noHeading"
|
:no-heading="noHeading"
|
||||||
:highlight="highlight"
|
:highlight="highlight"
|
||||||
:focused="isFocused"
|
:focused="isFocused"
|
||||||
|
:controlled-showing-tall="controlledShowingTall"
|
||||||
|
:controlled-expanding-subject="controlledExpandingSubject"
|
||||||
|
:controlled-showing-long-subject="controlledShowingLongSubject"
|
||||||
|
:controlled-toggle-showing-tall="controlledToggleShowingTall"
|
||||||
|
:controlled-toggle-expanding-subject="controlledToggleExpandingSubject"
|
||||||
|
:controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject"
|
||||||
@mediaplay="addMediaPlaying($event)"
|
@mediaplay="addMediaPlaying($event)"
|
||||||
@mediapause="removeMediaPlaying($event)"
|
@mediapause="removeMediaPlaying($event)"
|
||||||
@parseReady="setHeadTailLinks"
|
@parseReady="setHeadTailLinks"
|
||||||
|
@ -317,7 +348,20 @@
|
||||||
v-if="inConversation && !isPreview && replies && replies.length"
|
v-if="inConversation && !isPreview && replies && replies.length"
|
||||||
class="replies"
|
class="replies"
|
||||||
>
|
>
|
||||||
<span class="faint">{{ $t('status.replies_list') }}</span>
|
<button
|
||||||
|
v-if="showOtherRepliesAsButton && replies.length > 1"
|
||||||
|
class="button-unstyled -link faint"
|
||||||
|
:title="$tc('status.ancestor_follow', replies.length - 1, { numReplies: replies.length - 1 })"
|
||||||
|
@click.prevent="dive"
|
||||||
|
>
|
||||||
|
{{ $tc('status.replies_list_with_others', replies.length - 1, { numReplies: replies.length - 1 }) }}
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="faint"
|
||||||
|
>
|
||||||
|
{{ $t('status.replies_list') }}
|
||||||
|
</span>
|
||||||
<StatusPopover
|
<StatusPopover
|
||||||
v-for="reply in replies"
|
v-for="reply in replies"
|
||||||
:key="reply.id"
|
:key="reply.id"
|
||||||
|
|
|
@ -26,14 +26,16 @@ const StatusContent = {
|
||||||
'focused',
|
'focused',
|
||||||
'noHeading',
|
'noHeading',
|
||||||
'fullContent',
|
'fullContent',
|
||||||
'singleLine'
|
'singleLine',
|
||||||
|
'showingTall',
|
||||||
|
'expandingSubject',
|
||||||
|
'showingLongSubject',
|
||||||
|
'toggleShowingTall',
|
||||||
|
'toggleExpandingSubject',
|
||||||
|
'toggleShowingLongSubject'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
showingTall: this.fullContent || (this.inConversation && this.focused),
|
|
||||||
showingLongSubject: false,
|
|
||||||
// not as computed because it sets the initial state which will be changed later
|
|
||||||
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
|
|
||||||
postLength: this.status.text.length,
|
postLength: this.status.text.length,
|
||||||
parseReadyDone: false
|
parseReadyDone: false
|
||||||
}
|
}
|
||||||
|
@ -115,9 +117,9 @@ const StatusContent = {
|
||||||
},
|
},
|
||||||
toggleShowMore () {
|
toggleShowMore () {
|
||||||
if (this.mightHideBecauseTall) {
|
if (this.mightHideBecauseTall) {
|
||||||
this.showingTall = !this.showingTall
|
this.toggleShowingTall()
|
||||||
} else if (this.mightHideBecauseSubject) {
|
} else if (this.mightHideBecauseSubject) {
|
||||||
this.expandingSubject = !this.expandingSubject
|
this.toggleExpandingSubject()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
generateTagLink (tag) {
|
generateTagLink (tag) {
|
||||||
|
|
|
@ -17,14 +17,14 @@
|
||||||
<button
|
<button
|
||||||
v-if="longSubject && showingLongSubject"
|
v-if="longSubject && showingLongSubject"
|
||||||
class="button-unstyled -link tall-subject-hider"
|
class="button-unstyled -link tall-subject-hider"
|
||||||
@click.prevent="showingLongSubject=false"
|
@click.prevent="toggleShowingLongSubject"
|
||||||
>
|
>
|
||||||
{{ $t("status.hide_full_subject") }}
|
{{ $t("status.hide_full_subject") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-else-if="longSubject"
|
v-else-if="longSubject"
|
||||||
class="button-unstyled -link tall-subject-hider"
|
class="button-unstyled -link tall-subject-hider"
|
||||||
@click.prevent="showingLongSubject=true"
|
@click.prevent="toggleShowingLongSubject"
|
||||||
>
|
>
|
||||||
{{ $t("status.show_full_subject") }}
|
{{ $t("status.show_full_subject") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -23,6 +23,30 @@ library.add(
|
||||||
faPollH
|
faPollH
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const camelCase = name => name.charAt(0).toUpperCase() + name.slice(1)
|
||||||
|
|
||||||
|
const controlledOrUncontrolledGetters = list => list.reduce((res, name) => {
|
||||||
|
const camelized = camelCase(name)
|
||||||
|
const toggle = `controlledToggle${camelized}`
|
||||||
|
const controlledName = `controlled${camelized}`
|
||||||
|
const uncontrolledName = `uncontrolled${camelized}`
|
||||||
|
res[name] = function () {
|
||||||
|
return this[toggle] ? this[controlledName] : this[uncontrolledName]
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const controlledOrUncontrolledToggle = (obj, name) => {
|
||||||
|
const camelized = camelCase(name)
|
||||||
|
const toggle = `controlledToggle${camelized}`
|
||||||
|
const uncontrolledName = `uncontrolled${camelized}`
|
||||||
|
if (obj[toggle]) {
|
||||||
|
obj[toggle]()
|
||||||
|
} else {
|
||||||
|
obj[uncontrolledName] = !obj[uncontrolledName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const StatusContent = {
|
const StatusContent = {
|
||||||
name: 'StatusContent',
|
name: 'StatusContent',
|
||||||
props: [
|
props: [
|
||||||
|
@ -31,9 +55,22 @@ const StatusContent = {
|
||||||
'focused',
|
'focused',
|
||||||
'noHeading',
|
'noHeading',
|
||||||
'fullContent',
|
'fullContent',
|
||||||
'singleLine'
|
'singleLine',
|
||||||
|
'controlledShowingTall',
|
||||||
|
'controlledExpandingSubject',
|
||||||
|
'controlledToggleShowingTall',
|
||||||
|
'controlledToggleExpandingSubject'
|
||||||
],
|
],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
uncontrolledShowingTall: this.fullContent || (this.inConversation && this.focused),
|
||||||
|
uncontrolledShowingLongSubject: false,
|
||||||
|
// not as computed because it sets the initial state which will be changed later
|
||||||
|
uncontrolledExpandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...controlledOrUncontrolledGetters(['showingTall', 'expandingSubject', 'showingLongSubject']),
|
||||||
hideAttachments () {
|
hideAttachments () {
|
||||||
return (this.mergedConfig.hideAttachments && !this.inConversation) ||
|
return (this.mergedConfig.hideAttachments && !this.inConversation) ||
|
||||||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)
|
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)
|
||||||
|
@ -71,6 +108,21 @@ const StatusContent = {
|
||||||
Gallery,
|
Gallery,
|
||||||
LinkPreview,
|
LinkPreview,
|
||||||
StatusBody
|
StatusBody
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleShowingTall () {
|
||||||
|
controlledOrUncontrolledToggle(this, 'showingTall')
|
||||||
|
},
|
||||||
|
toggleExpandingSubject () {
|
||||||
|
controlledOrUncontrolledToggle(this, 'expandingSubject')
|
||||||
|
},
|
||||||
|
toggleShowingLongSubject () {
|
||||||
|
controlledOrUncontrolledToggle(this, 'showingLongSubject')
|
||||||
|
},
|
||||||
|
setMedia () {
|
||||||
|
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
|
||||||
|
return () => this.$store.dispatch('setMedia', attachments)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,12 @@
|
||||||
:status="status"
|
:status="status"
|
||||||
:compact="compact"
|
:compact="compact"
|
||||||
:single-line="singleLine"
|
:single-line="singleLine"
|
||||||
|
:showing-tall="showingTall"
|
||||||
|
:expanding-subject="expandingSubject"
|
||||||
|
:showing-long-subject="showingLongSubject"
|
||||||
|
:toggle-showing-tall="toggleShowingTall"
|
||||||
|
:toggle-expanding-subject="toggleExpandingSubject"
|
||||||
|
:toggle-showing-long-subject="toggleShowingLongSubject"
|
||||||
@parseReady="$emit('parseReady', $event)"
|
@parseReady="$emit('parseReady', $event)"
|
||||||
>
|
>
|
||||||
<div v-if="status.poll && status.poll.options && !compact">
|
<div v-if="status.poll && status.poll.options && !compact">
|
||||||
|
@ -52,10 +58,6 @@
|
||||||
|
|
||||||
<script src="./status_content.js" ></script>
|
<script src="./status_content.js" ></script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
$status-margin: 0.75em;
|
|
||||||
|
|
||||||
.StatusContent {
|
.StatusContent {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
90
src/components/thread_tree/thread_tree.js
Normal file
90
src/components/thread_tree/thread_tree.js
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import Status from '../status/status.vue'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faAngleDoubleDown,
|
||||||
|
faAngleDoubleRight
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faAngleDoubleDown,
|
||||||
|
faAngleDoubleRight
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
highlight: String,
|
||||||
|
getReplies: Function,
|
||||||
|
setHighlight: Function,
|
||||||
|
toggleExpanded: Function,
|
||||||
|
|
||||||
|
simple: Boolean,
|
||||||
|
// to control display of the whole thread forest
|
||||||
|
toggleThreadDisplay: Function,
|
||||||
|
threadDisplayStatus: Object,
|
||||||
|
showThreadRecursively: Function,
|
||||||
|
totalReplyCount: Object,
|
||||||
|
totalReplyDepth: Object,
|
||||||
|
statusContentProperties: Object,
|
||||||
|
setStatusContentProperty: Function,
|
||||||
|
toggleStatusContentProperty: Function,
|
||||||
|
dive: Function
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
suspendable () {
|
||||||
|
const selfSuspendable = this.$refs.statusComponent ? this.$refs.statusComponent.suspendable : true
|
||||||
|
if (this.$refs.childComponent) {
|
||||||
|
return selfSuspendable && this.$refs.childComponent.every(s => s.suspendable)
|
||||||
|
}
|
||||||
|
return selfSuspendable
|
||||||
|
},
|
||||||
|
reverseLookupTable () {
|
||||||
|
return this.conversation.reduce((table, status, index) => {
|
||||||
|
table[status.id] = index
|
||||||
|
return table
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
currentReplies () {
|
||||||
|
return this.getReplies(this.status.id).map(({ id }) => this.statusById(id))
|
||||||
|
},
|
||||||
|
threadShowing () {
|
||||||
|
return this.threadDisplayStatus[this.status.id] === 'showing'
|
||||||
|
},
|
||||||
|
currentProp () {
|
||||||
|
return this.statusContentProperties[this.status.id]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
statusById (id) {
|
||||||
|
return this.conversation[this.reverseLookupTable[id]]
|
||||||
|
},
|
||||||
|
collapseThread () {
|
||||||
|
},
|
||||||
|
showThread () {
|
||||||
|
},
|
||||||
|
showAllSubthreads () {
|
||||||
|
},
|
||||||
|
toggleCurrentProp (name) {
|
||||||
|
this.toggleStatusContentProperty(this.status.id, name)
|
||||||
|
},
|
||||||
|
setCurrentProp (name, newVal) {
|
||||||
|
this.setStatusContentProperty(this.status.id, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThreadTree
|
127
src/components/thread_tree/thread_tree.vue
Normal file
127
src/components/thread_tree/thread_tree.vue
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
<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="highlight"
|
||||||
|
:replies="getReplies(status.id)"
|
||||||
|
:in-profile="inProfile"
|
||||||
|
:profile-user-id="profileUserId"
|
||||||
|
class="conversation-status conversation-status-treeview status-fadein panel-body"
|
||||||
|
|
||||||
|
:simple-tree="simple"
|
||||||
|
:controlled-thread-display-status="threadDisplayStatus[status.id]"
|
||||||
|
:controlled-toggle-thread-display="() => toggleThreadDisplay(status.id)"
|
||||||
|
|
||||||
|
:controlled-showing-tall="currentProp.showingTall"
|
||||||
|
:controlled-expanding-subject="currentProp.expandingSubject"
|
||||||
|
:controlled-showing-long-subject="currentProp.showingLongSubject"
|
||||||
|
:controlled-replying="currentProp.replying"
|
||||||
|
:controlled-media-playing="currentProp.mediaPlaying"
|
||||||
|
:controlled-toggle-showing-tall="() => toggleCurrentProp('showingTall')"
|
||||||
|
:controlled-toggle-expanding-subject="() => toggleCurrentProp('expandingSubject')"
|
||||||
|
:controlled-toggle-showing-long-subject="() => toggleCurrentProp('showingLongSubject')"
|
||||||
|
:controlled-toggle-replying="() => toggleCurrentProp('replying')"
|
||||||
|
:controlled-set-media-playing="(newVal) => setCurrentProp('mediaPlaying', newVal)"
|
||||||
|
:dive="dive ? () => dive(status.id) : undefined"
|
||||||
|
|
||||||
|
@goto="setHighlight"
|
||||||
|
@toggleExpanded="toggleExpanded"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
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"
|
||||||
|
:conversation="conversation"
|
||||||
|
:collapsable="collapsable"
|
||||||
|
:is-expanded="isExpanded"
|
||||||
|
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||||
|
:profile-user-id="profileUserId"
|
||||||
|
|
||||||
|
:focused="focused"
|
||||||
|
:get-replies="getReplies"
|
||||||
|
:highlight="highlight"
|
||||||
|
:set-highlight="setHighlight"
|
||||||
|
:toggle-expanded="toggleExpanded"
|
||||||
|
|
||||||
|
:simple="simple"
|
||||||
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
|
:thread-display-status="threadDisplayStatus"
|
||||||
|
:show-thread-recursively="showThreadRecursively"
|
||||||
|
:total-reply-count="totalReplyCount"
|
||||||
|
:total-reply-depth="totalReplyDepth"
|
||||||
|
:status-content-properties="statusContentProperties"
|
||||||
|
:set-status-content-property="setStatusContentProperty"
|
||||||
|
:toggle-status-content-property="toggleStatusContentProperty"
|
||||||
|
:dive="dive"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="currentReplies.length && !threadShowing"
|
||||||
|
class="thread-tree-replies thread-tree-replies-hidden"
|
||||||
|
>
|
||||||
|
<i18n
|
||||||
|
v-if="simple"
|
||||||
|
tag="button"
|
||||||
|
path="status.thread_follow_with_icon"
|
||||||
|
class="button-unstyled -link thread-tree-show-replies-button"
|
||||||
|
@click.prevent="dive(status.id)"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
place="icon"
|
||||||
|
icon="angle-double-right"
|
||||||
|
/>
|
||||||
|
<span place="text">
|
||||||
|
{{ $tc('status.thread_follow', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id] }) }}
|
||||||
|
</span>
|
||||||
|
</i18n>
|
||||||
|
<i18n
|
||||||
|
v-else
|
||||||
|
tag="button"
|
||||||
|
path="status.thread_show_full_with_icon"
|
||||||
|
class="button-unstyled -link thread-tree-show-replies-button"
|
||||||
|
@click.prevent="showThreadRecursively(status.id)"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
place="icon"
|
||||||
|
icon="angle-double-down"
|
||||||
|
/>
|
||||||
|
<span place="text">
|
||||||
|
{{ $tc('status.thread_show_full', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id], depth: totalReplyDepth[status.id] }) }}
|
||||||
|
</span>
|
||||||
|
</i18n>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./thread_tree.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
.thread-tree-replies {
|
||||||
|
margin-left: var(--status-margin, $status-margin);
|
||||||
|
border-left: 2px solid var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-tree-replies-hidden {
|
||||||
|
padding: var(--status-margin, $status-margin);
|
||||||
|
/* Make the button stretch along the whole row */
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -468,6 +468,15 @@
|
||||||
"subject_line_email": "Like email: \"re: subject\"",
|
"subject_line_email": "Like email: \"re: subject\"",
|
||||||
"subject_line_mastodon": "Like mastodon: copy as is",
|
"subject_line_mastodon": "Like mastodon: copy as is",
|
||||||
"subject_line_noop": "Do not copy",
|
"subject_line_noop": "Do not copy",
|
||||||
|
"conversation_display": "Conversation display style",
|
||||||
|
"conversation_display_tree": "Tree-style",
|
||||||
|
"tree_advanced": "Allow more flexible navigation in tree view",
|
||||||
|
"tree_fade_ancestors": "Display ancestors of the current status in faint text",
|
||||||
|
"conversation_display_linear": "Linear-style",
|
||||||
|
"conversation_other_replies_button": "Show the \"other replies\" button",
|
||||||
|
"conversation_other_replies_button_below": "Below statuses",
|
||||||
|
"conversation_other_replies_button_inside": "Inside statuses",
|
||||||
|
"max_depth_in_thread": "Maximum number of levels in thread to display by default",
|
||||||
"post_status_content_type": "Post status content type",
|
"post_status_content_type": "Post status content type",
|
||||||
"sensitive_by_default": "Mark posts as sensitive by default",
|
"sensitive_by_default": "Mark posts as sensitive by default",
|
||||||
"stop_gifs": "Pause animated images until you hover on them",
|
"stop_gifs": "Pause animated images until you hover on them",
|
||||||
|
@ -724,6 +733,7 @@
|
||||||
"reply_to": "Reply to",
|
"reply_to": "Reply to",
|
||||||
"mentions": "Mentions",
|
"mentions": "Mentions",
|
||||||
"replies_list": "Replies:",
|
"replies_list": "Replies:",
|
||||||
|
"replies_list_with_others": "Replies (+{numReplies} other): | Replies (+{numReplies} others):",
|
||||||
"mute_conversation": "Mute conversation",
|
"mute_conversation": "Mute conversation",
|
||||||
"unmute_conversation": "Unmute conversation",
|
"unmute_conversation": "Unmute conversation",
|
||||||
"status_unavailable": "Status unavailable",
|
"status_unavailable": "Status unavailable",
|
||||||
|
@ -750,7 +760,18 @@
|
||||||
"attachment_stop_flash": "Stop Flash player",
|
"attachment_stop_flash": "Stop Flash player",
|
||||||
"move_up": "Shift attachment left",
|
"move_up": "Shift attachment left",
|
||||||
"move_down": "Shift attachment right",
|
"move_down": "Shift attachment right",
|
||||||
"open_gallery": "Open gallery"
|
"open_gallery": "Open gallery",
|
||||||
|
"thread_hide": "Hide this thread",
|
||||||
|
"thread_show": "Show this thread",
|
||||||
|
"thread_show_full": "Show everything under this thread ({numStatus} status in total, max depth {depth}) | Show everything under this thread ({numStatus} statuses in total, max depth {depth})",
|
||||||
|
"thread_show_full_with_icon": "{icon} {text}",
|
||||||
|
"thread_follow": "See the remaining part of this thread ({numStatus} status in total) | See the remaining part of this thread ({numStatus} statuses in total)",
|
||||||
|
"thread_follow_with_icon": "{icon} {text}",
|
||||||
|
"ancestor_follow": "See {numReplies} other reply under this status | See {numReplies} other replies under this status",
|
||||||
|
"ancestor_follow_with_icon": "{icon} {text}",
|
||||||
|
"show_all_conversation_with_icon": "{icon} {text}",
|
||||||
|
"show_all_conversation": "Show full conversation ({numStatus} other status) | Show full conversation ({numStatus} other statuses)",
|
||||||
|
"show_only_conversation_under_this": "Only show replies to this status"
|
||||||
},
|
},
|
||||||
"user_card": {
|
"user_card": {
|
||||||
"approve": "Approve",
|
"approve": "Approve",
|
||||||
|
|
|
@ -12,6 +12,8 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0]
|
||||||
export const multiChoiceProperties = [
|
export const multiChoiceProperties = [
|
||||||
'postContentType',
|
'postContentType',
|
||||||
'subjectLineBehavior',
|
'subjectLineBehavior',
|
||||||
|
'conversationDisplay', // tree | linear
|
||||||
|
'conversationOtherRepliesButton', // below | inside
|
||||||
'mentionLinkDisplay' // short | full_for_remote | full
|
'mentionLinkDisplay' // short | full_for_remote | full
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -83,7 +85,12 @@ export const defaultState = {
|
||||||
hideBotIndication: undefined, // instance default
|
hideBotIndication: 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
|
||||||
|
conversationTreeAdvanced: undefined, // instance default
|
||||||
|
conversationOtherRepliesButton: undefined, // instance default
|
||||||
|
conversationTreeFadeAncestors: undefined, // instance default
|
||||||
|
maxDepthInThread: undefined // instance default
|
||||||
}
|
}
|
||||||
|
|
||||||
// caching the instance default properties
|
// caching the instance default properties
|
||||||
|
|
|
@ -55,6 +55,11 @@ const defaultState = {
|
||||||
theme: 'pleroma-dark',
|
theme: 'pleroma-dark',
|
||||||
virtualScrolling: true,
|
virtualScrolling: true,
|
||||||
sensitiveByDefault: false,
|
sensitiveByDefault: false,
|
||||||
|
conversationDisplay: 'linear',
|
||||||
|
conversationTreeAdvanced: false,
|
||||||
|
conversationOtherRepliesButton: 'below',
|
||||||
|
conversationTreeFadeAncestors: false,
|
||||||
|
maxDepthInThread: 6,
|
||||||
|
|
||||||
// Nasty stuff
|
// Nasty stuff
|
||||||
customEmoji: [],
|
customEmoji: [],
|
||||||
|
|
Loading…
Reference in a new issue