|
@@ -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,13 +70,51 @@ const conversation = {
|
|
}
|
|
}
|
|
},
|
|
},
|
|
computed: {
|
|
computed: {
|
|
- hideStatus () {
|
|
|
|
|
|
+ maxDepthToShowByDefault () {
|
|
|
|
+ // maxDepthInThread = max number of depths that is *visible*
|
|
|
|
+ // since our depth starts with 0 and "showing" means "showing children"
|
|
|
|
+ // there is a -2 here
|
|
|
|
+ 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]) {
|
|
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
|
|
- return this.virtualHidden && this.$refs.statusComponent[0].suspendable
|
|
|
|
|
|
+ return this.$refs.statusComponent.every(s => s.suspendable)
|
|
} else {
|
|
} else {
|
|
- return this.virtualHidden
|
|
|
|
|
|
+ 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
|
|
@@ -101,7 +271,7 @@ const conversation = {
|
|
result[irid] = result[irid] || []
|
|
result[irid] = result[irid] || []
|
|
result[irid].push({
|
|
result[irid].push({
|
|
name: `#${i}`,
|
|
name: `#${i}`,
|
|
- id: id
|
|
|
|
|
|
+ id
|
|
})
|
|
})
|
|
}
|
|
}
|
|
i++
|
|
i++
|
|
@@ -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) {
|
|
|
|
- return (this.isExpanded) && id === this.statusId
|
|
|
|
|
|
+ getHighlight () {
|
|
|
|
+ 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 = {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|