massive revision of conversation component, specificially threading stuff
This commit is contained in:
parent
78e40e4ce2
commit
74f8ef0719
11 changed files with 469 additions and 577 deletions
|
|
@ -83,8 +83,9 @@ export default () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'conversation',
|
name: 'conversation',
|
||||||
path: '/notice/:id',
|
path: '/notice/:statusId',
|
||||||
component: ConversationPage,
|
component: ConversationPage,
|
||||||
|
props: true,
|
||||||
meta: { dontScroll: true },
|
meta: { dontScroll: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { get, reduce } from 'lodash-es'
|
import { get, reduce } from 'lodash-es'
|
||||||
import { mapState } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { computed, nextTick, ref, watch, toRefs } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
|
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
|
||||||
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
||||||
|
|
@ -14,7 +16,7 @@ import { useOAuthStore } from 'src/stores/oauth.js'
|
||||||
import { useStatusesStore } from 'src/stores/statuses.js'
|
import { useStatusesStore } from 'src/stores/statuses.js'
|
||||||
import { useStreamingStore } from 'src/stores/streaming.js'
|
import { useStreamingStore } from 'src/stores/streaming.js'
|
||||||
|
|
||||||
import { fetchConversation, fetchStatus } from 'src/api/public.js'
|
import { fetchConversation as apiFetchConversation, fetchStatus } from 'src/api/public.js'
|
||||||
import { WSConnectionStatus } from 'src/api/websocket.js'
|
import { WSConnectionStatus } from 'src/api/websocket.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
|
@ -52,7 +54,7 @@ const sortById = (a, b) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversation = {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
statusId: {
|
statusId: {
|
||||||
// Main thing
|
// Main thing
|
||||||
|
|
@ -94,313 +96,6 @@ const conversation = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ['update:virtualHeight'],
|
emits: ['update:virtualHeight'],
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
focused: null,
|
|
||||||
expanded: false,
|
|
||||||
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
|
|
||||||
inlineDivePosition: null,
|
|
||||||
loadStatusError: null,
|
|
||||||
unsuspendibleIds: new Set(),
|
|
||||||
virtualHeight: 120,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
if (this.isPage) {
|
|
||||||
this.fetchConversation()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.updateVirtualHeight()
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
status() {
|
|
||||||
return useStatusesStore().allStatuses.get(this.statusId)
|
|
||||||
},
|
|
||||||
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.mergedConfig.maxDepthInThread - 2
|
|
||||||
return maxDepth >= 1 ? maxDepth : 1
|
|
||||||
},
|
|
||||||
streamingEnabled() {
|
|
||||||
return (
|
|
||||||
this.mergedConfig.useStreamingApi &&
|
|
||||||
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
|
||||||
)
|
|
||||||
},
|
|
||||||
displayStyle() {
|
|
||||||
return this.mergedConfig.conversationDisplay
|
|
||||||
},
|
|
||||||
treeViewIsSimple() {
|
|
||||||
return !this.mergedConfig.conversationTreeAdvanced
|
|
||||||
},
|
|
||||||
isTreeView() {
|
|
||||||
return this.displayStyle === 'tree'
|
|
||||||
},
|
|
||||||
isLinearView() {
|
|
||||||
return this.displayStyle !== 'tree'
|
|
||||||
},
|
|
||||||
shouldFadeAncestors() {
|
|
||||||
return this.mergedConfig.conversationTreeFadeAncestors
|
|
||||||
},
|
|
||||||
otherRepliesButtonPosition() {
|
|
||||||
return this.mergedConfig.conversationOtherRepliesButton
|
|
||||||
},
|
|
||||||
showOtherRepliesButtonBelowStatus() {
|
|
||||||
return this.otherRepliesButtonPosition === 'below'
|
|
||||||
},
|
|
||||||
showOtherRepliesButtonInsideStatus() {
|
|
||||||
return this.otherRepliesButtonPosition === 'inside'
|
|
||||||
},
|
|
||||||
suspendable() {
|
|
||||||
return this.unsuspendibleIds.size === 0
|
|
||||||
},
|
|
||||||
hide() {
|
|
||||||
return this.virtualHidden && this.suspendable
|
|
||||||
},
|
|
||||||
originalStatusId() {
|
|
||||||
if (this.status.retweeted_status) {
|
|
||||||
return this.status.retweeted_status.id
|
|
||||||
} else {
|
|
||||||
return this.statusId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
conversationId() {
|
|
||||||
return this.getConversationId(this.statusId)
|
|
||||||
},
|
|
||||||
conversation() {
|
|
||||||
if (!this.status) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isExpanded) {
|
|
||||||
return [this.status]
|
|
||||||
}
|
|
||||||
|
|
||||||
const conversation = useStatusesStore().conversations.get(
|
|
||||||
this.conversationId,
|
|
||||||
)
|
|
||||||
|
|
||||||
return [...conversation.keys()]
|
|
||||||
.map((k) => useStatusesStore().allStatuses.get(k))
|
|
||||||
.filter((status) => status.type != 'repeat') // Old backend behavior?
|
|
||||||
.toSorted(sortById)
|
|
||||||
},
|
|
||||||
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),
|
|
||||||
].flat()
|
|
||||||
})
|
|
||||||
.flat()
|
|
||||||
|
|
||||||
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)
|
|
||||||
.includes(k.id),
|
|
||||||
),
|
|
||||||
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() {
|
|
||||||
let i = 1
|
|
||||||
|
|
||||||
return reduce(
|
|
||||||
this.conversation,
|
|
||||||
(result, { id, in_reply_to_status_id: irid }) => {
|
|
||||||
if (irid) {
|
|
||||||
result[irid] = result[irid] || []
|
|
||||||
result[irid].push({
|
|
||||||
name: `#${i}`,
|
|
||||||
id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
isExpanded() {
|
|
||||||
return !!(this.expanded || this.isPage)
|
|
||||||
},
|
|
||||||
hiddenStyle() {
|
|
||||||
return { height: this.virtualHeight + 'px' }
|
|
||||||
},
|
|
||||||
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
|
|
||||||
}, {})
|
|
||||||
},
|
|
||||||
canDive() {
|
|
||||||
return this.isTreeView && this.isExpanded
|
|
||||||
},
|
|
||||||
maybeFocused() {
|
|
||||||
return this.isExpanded ? this.focused : null
|
|
||||||
},
|
|
||||||
...mapState(useMergedConfigStore, ['mergedConfig']),
|
|
||||||
...mapState(useStreamingStore, {
|
|
||||||
mastoUserSocketStatus: (state) => state.state,
|
|
||||||
}),
|
|
||||||
...mapState(useInterfaceStore, {
|
|
||||||
mobileLayout: (store) => store.layoutType === 'mobile',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
ThreadTree,
|
ThreadTree,
|
||||||
QuickFilterSettings,
|
QuickFilterSettings,
|
||||||
|
|
@ -409,149 +104,316 @@ const conversation = {
|
||||||
PostStatusForm,
|
PostStatusForm,
|
||||||
RichContent,
|
RichContent,
|
||||||
},
|
},
|
||||||
watch: {
|
setup(props, ctx) {
|
||||||
statusId(newVal, oldVal) {
|
const { emit } = ctx
|
||||||
const newConversationId = this.getConversationId(newVal)
|
const {
|
||||||
const oldConversationId = this.getConversationId(oldVal)
|
statusId,
|
||||||
if (
|
collapsable,
|
||||||
newConversationId &&
|
pinnedStatusIdsObject,
|
||||||
oldConversationId &&
|
inProfile,
|
||||||
newConversationId === oldConversationId
|
profileUserId,
|
||||||
) {
|
virtualHidden,
|
||||||
this.setFocused(this.originalStatusId)
|
} = toRefs(props)
|
||||||
} else {
|
const router = useRouter()
|
||||||
this.fetchConversation()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expanded(value) {
|
|
||||||
if (value) {
|
|
||||||
this.fetchConversation()
|
|
||||||
} else {
|
|
||||||
this.resetDisplayState()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
virtualHidden() {
|
|
||||||
this.updateVirtualHeight()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchConversation() {
|
|
||||||
if (this.status) {
|
|
||||||
fetchConversation({
|
|
||||||
id: this.statusId,
|
|
||||||
credentials: useOAuthStore().token,
|
|
||||||
}).then(({ data: { ancestors, descendants }, timestamp }) => {
|
|
||||||
useStatusesStore().addNewStatuses({ statuses: ancestors, timestamp })
|
|
||||||
useStatusesStore().addNewStatuses({
|
|
||||||
statuses: descendants,
|
|
||||||
timestamp,
|
|
||||||
})
|
|
||||||
this.setFocused(this.originalStatusId)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.loadStatusError = null
|
|
||||||
fetchStatus({
|
|
||||||
id: this.statusId,
|
|
||||||
credentials: useOAuthStore().token,
|
|
||||||
})
|
|
||||||
.then(({ data: status }) => {
|
|
||||||
useStatusesStore().addNewStatuses({ statuses: [status] })
|
|
||||||
this.fetchConversation()
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error)
|
|
||||||
this.loadStatusError = error
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getReplies(id) {
|
|
||||||
return this.replies[id] || []
|
|
||||||
},
|
|
||||||
setFocused(id) {
|
|
||||||
if (!id) return
|
|
||||||
this.focused = id
|
|
||||||
|
|
||||||
if (!this.streamingEnabled) {
|
const hoisted = {}
|
||||||
useStatusesStore().fetchStatus(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
useStatusesStore().fetchFavsAndRepeats(id)
|
// # Main Configuration
|
||||||
useStatusesStore().fetchEmojiReactions(id)
|
const { mergedConfig } = storeToRefs(useMergedConfigStore())
|
||||||
},
|
const { mastoUserSocketStatus } = storeToRefs(useStreamingStore())
|
||||||
toggleExpanded() {
|
const displayStyle = computed(() => mergedConfig.value.conversationDisplay)
|
||||||
this.expanded = !this.expanded
|
const streamingEnabled = computed(() =>
|
||||||
},
|
mergedConfig.value.useStreamingApi &&
|
||||||
getConversationId(statusId) {
|
mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||||
const status = useStatusesStore().allStatuses.get(statusId)
|
)
|
||||||
|
|
||||||
|
// # Main things
|
||||||
|
const getStatusObject = (id) => useStatusesStore().allStatuses.get(id)
|
||||||
|
const getConversationId = (statusId) => {
|
||||||
|
const status = getStatusObject(statusId)
|
||||||
return get(
|
return get(
|
||||||
status,
|
status,
|
||||||
'retweeted_status.statusnet_conversation_id',
|
'retweeted_status.statusnet_conversation_id',
|
||||||
get(status, 'statusnet_conversation_id'),
|
get(status, 'statusnet_conversation_id'),
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
setThreadDisplay(id, nextStatus) {
|
|
||||||
this.threadDisplayStatusObject = {
|
const status = computed(() => getStatusObject(statusId.value))
|
||||||
...this.threadDisplayStatusObject,
|
const mainStatusId = computed(() => {
|
||||||
[id]: nextStatus,
|
if (status.value.retweeted_status) {
|
||||||
|
return status.value.retweeted_status.id
|
||||||
|
} else {
|
||||||
|
return statusId.value
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
getStatusClasses(status, active) {
|
const conversationId = computed(() => getConversationId(statusId.value))
|
||||||
return {
|
const conversation = computed(() => {
|
||||||
'-virtual-active': active,
|
if (!status.value) {
|
||||||
'-last': status.id === this.conversation[this.conversation.length - 1].id,
|
return []
|
||||||
'-first': status.id === this.conversation[0].id,
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
toggleThreadDisplay(id) {
|
if (!isExpanded.value) {
|
||||||
const curStatus = this.threadDisplayStatus[id]
|
return [status.value]
|
||||||
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')
|
|
||||||
},
|
|
||||||
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
|
const conversation = useStatusesStore().conversations.get(
|
||||||
},
|
conversationId.value,
|
||||||
diveIntoStatus(id) {
|
|
||||||
this.tryScrollTo(id)
|
|
||||||
},
|
|
||||||
diveToTopLevel() {
|
|
||||||
this.tryScrollTo(
|
|
||||||
this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id,
|
|
||||||
)
|
)
|
||||||
},
|
|
||||||
// only used when we are not on a page
|
return [...conversation.keys()]
|
||||||
undive() {
|
.map((k) => useStatusesStore().allStatuses.get(k))
|
||||||
this.inlineDivePosition = null
|
.filter((status) => status.type != 'repeat') // Old backend behavior?
|
||||||
this.setFocused(this.statusId)
|
.toSorted(sortById)
|
||||||
},
|
})
|
||||||
tryScrollTo(id) {
|
const replies = computed(() =>
|
||||||
|
conversation.value.reduce(
|
||||||
|
(
|
||||||
|
result,
|
||||||
|
{ id, in_reply_to_status_id: irid },
|
||||||
|
index,
|
||||||
|
) => {
|
||||||
|
if (irid) {
|
||||||
|
if (!result.has(irid)) {
|
||||||
|
result.set(irid, new Set())
|
||||||
|
}
|
||||||
|
result.get(irid).add({
|
||||||
|
name: `#${index}`,
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
new Map(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const getReplies = (id) => replies.value.get(id) ?? new Set()
|
||||||
|
|
||||||
|
const fetchConversation = async () => {
|
||||||
|
if (status.value) {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
ancestors,
|
||||||
|
descendants
|
||||||
|
},
|
||||||
|
timestamp
|
||||||
|
} = await apiFetchConversation({
|
||||||
|
id: statusId.value,
|
||||||
|
credentials: useOAuthStore().token,
|
||||||
|
})
|
||||||
|
|
||||||
|
useStatusesStore().addNewStatuses({ statuses: ancestors, timestamp })
|
||||||
|
useStatusesStore().addNewStatuses({
|
||||||
|
statuses: descendants,
|
||||||
|
timestamp,
|
||||||
|
})
|
||||||
|
setFocused(mainStatusId.value)
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
loadStatusError.value = null
|
||||||
|
|
||||||
|
const { data: status } = await fetchStatus({
|
||||||
|
id: statusId.value,
|
||||||
|
credentials: useOAuthStore().token,
|
||||||
|
})
|
||||||
|
|
||||||
|
useStatusesStore().addNewStatuses({ statuses: [status] })
|
||||||
|
fetchConversation()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
loadStatusError.value = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const resetDisplayState = () => {
|
||||||
|
hoisted.undive()
|
||||||
|
threadDisplay.value = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
// # Virtual scrolling stuff
|
||||||
|
const unsuspendibleIds = ref(new Set())
|
||||||
|
const suspendable = computed(() => unsuspendibleIds.value.size === 0)
|
||||||
|
const hide = computed(() => virtualHidden.value && suspendable.value)
|
||||||
|
const onStatusSuspendStateChange = ({ id, suspend }) => {
|
||||||
|
if (!suspend) {
|
||||||
|
unsuspendibleIds.value.add(id)
|
||||||
|
} else {
|
||||||
|
unsuspendibleIds.value.delete(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// # Misc UI things
|
||||||
|
const loadStatusError = ref(null)
|
||||||
|
|
||||||
|
const { layoutType } = storeToRefs(useInterfaceStore())
|
||||||
|
const mobileLayout = computed(() => layoutType.value === 'mobile')
|
||||||
|
const firstStatus = computed(() => conversation.value[0])
|
||||||
|
const lastStatus = computed(() => conversation.value[conversation.value.legnth - 1])
|
||||||
|
const getStatusClasses = (status, active) => ({
|
||||||
|
'-virtual-active': active,
|
||||||
|
'-first': status.id === firstStatus.value?.id,
|
||||||
|
'-last': status.id === lastStatus.value?.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
// # Conversation Expansion
|
||||||
|
const expanded = ref(false)
|
||||||
|
const { isPage } = toRefs(props)
|
||||||
|
const isExpanded = computed(() => !!(expanded.value || isPage.value))
|
||||||
|
const toggleExpanded = () => {
|
||||||
|
expanded.value = !expanded.value
|
||||||
|
}
|
||||||
|
watch(expanded, (value) => {
|
||||||
|
if (value) {
|
||||||
|
fetchConversation()
|
||||||
|
} else {
|
||||||
|
resetDisplayState()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// # Focus
|
||||||
|
const focused = ref(null)
|
||||||
|
const maybeFocused = computed(() => isExpanded.value ? focused.value : null)
|
||||||
|
const setFocused = (id) => {
|
||||||
|
if (!id) return
|
||||||
|
focused.value = id
|
||||||
|
|
||||||
|
if (!streamingEnabled.value) {
|
||||||
|
useStatusesStore().fetchStatus(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
useStatusesStore().fetchFavsAndRepeats(id)
|
||||||
|
useStatusesStore().fetchEmojiReactions(id)
|
||||||
|
}
|
||||||
|
watch(statusId, (newVal, oldVal) => {
|
||||||
|
const newConversationId = getConversationId(newVal)
|
||||||
|
const oldConversationId = getConversationId(oldVal)
|
||||||
|
if (
|
||||||
|
newConversationId &&
|
||||||
|
oldConversationId &&
|
||||||
|
newConversationId === oldConversationId
|
||||||
|
) {
|
||||||
|
setFocused(mainStatusId.value)
|
||||||
|
} else {
|
||||||
|
fetchConversation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Component created
|
||||||
|
if (isPage.value) {
|
||||||
|
fetchConversation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// # Linear style stuff
|
||||||
|
const isLinearView = computed(() => displayStyle.value !== 'tree')
|
||||||
|
|
||||||
|
// # Tree style stuff
|
||||||
|
const isTreeView = computed(() => displayStyle.value === 'tree')
|
||||||
|
|
||||||
|
// ## Tree view settings
|
||||||
|
const treeViewIsSimple = computed(() => !mergedConfig.value.conversationTreeAdvanced)
|
||||||
|
const shouldFadeAncestors = computed(() => mergedConfig.value.conversationTreeFadeAncestors)
|
||||||
|
const otherRepliesButtonPosition = computed(() => mergedConfig.value.conversationOtherRepliesButton)
|
||||||
|
const showOtherRepliesButtonBelowStatus = computed(() => otherRepliesButtonPosition.value === 'below')
|
||||||
|
const showOtherRepliesButtonInsideStatus = computed(() => otherRepliesButtonPosition.value === 'inside')
|
||||||
|
const maxDepthToShowByDefault = computed(() => {
|
||||||
|
// 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 = mergedConfig.value.maxDepthInThread - 2
|
||||||
|
return Math.min(1, maxDepth)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ## Tree style state
|
||||||
|
// ### Dive
|
||||||
|
const inlineDivePosition = ref(null)
|
||||||
|
const currentStatusId = computed(() => inlineDivePosition.value ?? statusId.value)
|
||||||
|
|
||||||
|
// ### Topology
|
||||||
|
const ancestors = computed(() => {
|
||||||
|
// First we fill map with empty sets and add given id's parent
|
||||||
|
// as set's only element (if any)
|
||||||
|
const parentMap = conversation.value.reduce(
|
||||||
|
(
|
||||||
|
result,
|
||||||
|
{ id, in_reply_to_status_id: irid },
|
||||||
|
) => {
|
||||||
|
if (!result.has(id)) {
|
||||||
|
result.set(id, new Set())
|
||||||
|
}
|
||||||
|
if (irid) {
|
||||||
|
// Setting parent for current item
|
||||||
|
result.get(id).add(irid)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
new Map()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Next we iterate over each entry and fill the whole ancestry chain
|
||||||
|
parentMap.entries().forEach(([originId, originSet]) => {
|
||||||
|
let current = originSet.values().next().value
|
||||||
|
while (current) {
|
||||||
|
originSet.add(current)
|
||||||
|
|
||||||
|
const parent = parentMap.get(current) ?? new Set()
|
||||||
|
|
||||||
|
current = parent.values().next().value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return parentMap
|
||||||
|
})
|
||||||
|
const getAncestorIds = (id) => ancestors.value.get(id) ?? new Set()
|
||||||
|
const getAncestors = (id) => [...getAncestorIds(id)].map(getStatusObject).filter(Boolean)
|
||||||
|
const currentAncestors = computed(() => getAncestors(currentStatusId.value).reverse())
|
||||||
|
const currentDepth = computed(() => currentAncestors.value.length)
|
||||||
|
const topLevel = computed(() => [...ancestors.value.entries()]
|
||||||
|
.filter(([id, ancestors]) => ancestors.size === 0)
|
||||||
|
.map(([id]) => getStatusObject(id))
|
||||||
|
)
|
||||||
|
const currentStatus = computed(() => getStatusObject(currentStatusId.value))
|
||||||
|
|
||||||
|
// ### Thread Display
|
||||||
|
const threadDisplay = ref(new Map()) // id => 'showing' | 'hidden'
|
||||||
|
const threadDisplayDefault = computed(() => {
|
||||||
|
return conversation.value.reduce((map, status) => {
|
||||||
|
const { id } = status
|
||||||
|
const depth = ancestors.value.get(id).size
|
||||||
|
|
||||||
|
const state = (() => {
|
||||||
|
if (depth - currentDepth.value <= maxDepthToShowByDefault.value) {
|
||||||
|
return 'showing'
|
||||||
|
} else {
|
||||||
|
return 'hidden'
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
map.set(id, state)
|
||||||
|
return map
|
||||||
|
}, new Map())
|
||||||
|
})
|
||||||
|
|
||||||
|
const setThreadDisplayRecursively = (id, value) => {
|
||||||
|
threadDisplay.value.set(id, value)
|
||||||
|
;[...getReplies(id)]
|
||||||
|
.map((k) => k.id)
|
||||||
|
.map((id) => setThreadDisplayRecursively(id, value))
|
||||||
|
}
|
||||||
|
const showThreadRecursively = (id) => {
|
||||||
|
setThreadDisplayRecursively(id, 'showing')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ## Derived values
|
||||||
|
const shouldShowAllConversationButton = computed(() => currentAncestors.value.length > 0 && topLevel.value.length > 1)
|
||||||
|
const shouldShowAncestors = computed(() => isExpanded.value && ancestors.value.get(currentStatusId.value) != null)
|
||||||
|
|
||||||
|
// # Scrolling / diving
|
||||||
|
const tryScrollTo = (id) => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.isPage) {
|
if (isPage.value) {
|
||||||
// set statusId
|
router.push({ name: 'conversation', params: { statusId: id } })
|
||||||
this.$router.push({ name: 'conversation', params: { id } })
|
|
||||||
} else {
|
} else {
|
||||||
this.inlineDivePosition = id
|
inlineDivePosition.value = id
|
||||||
}
|
}
|
||||||
// Because the conversation can be unmounted when out of sight
|
// Because the conversation can be unmounted when out of sight
|
||||||
// and mounted again when it comes into sight,
|
// and mounted again when it comes into sight,
|
||||||
|
|
@ -568,78 +430,64 @@ const conversation = {
|
||||||
// different).
|
// different).
|
||||||
// Here, let the components be rendered first, in order to trigger
|
// Here, let the components be rendered first, in order to trigger
|
||||||
// the `focused` watcher.
|
// the `focused` watcher.
|
||||||
this.$nextTick(() => {
|
nextTick(() => {
|
||||||
this.setFocused(id)
|
setFocused(id)
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
goToCurrent() {
|
const goToCurrent = () => {
|
||||||
this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
|
tryScrollTo(diveRoot)
|
||||||
},
|
}
|
||||||
statusById(id) {
|
const diveIntoStatus = (id) => {
|
||||||
return this.statusMap[id]
|
tryScrollTo(id)
|
||||||
},
|
}
|
||||||
parentOf(id) {
|
const diveToTopLevel = () => {
|
||||||
const status = this.statusById(id)
|
tryScrollTo(currentAncestors.value[0].id)
|
||||||
if (!status) {
|
}
|
||||||
return undefined
|
const undive = () => {
|
||||||
}
|
inlineDivePosition.value = null
|
||||||
const { in_reply_to_status_id: parentId } = status
|
setFocused(statusId.value)
|
||||||
if (!this.statusMap[parentId]) {
|
}
|
||||||
return undefined
|
hoisted.undive = undive
|
||||||
}
|
|
||||||
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 = {}
|
|
||||||
},
|
|
||||||
onStatusSuspendStateChange({ id, suspend }) {
|
|
||||||
if (!suspend) {
|
|
||||||
this.unsuspendibleIds.add(id)
|
|
||||||
} else {
|
|
||||||
this.unsuspendibleIds.delete(id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPosted(data) {
|
|
||||||
if (this.isPage) {
|
|
||||||
this.$router.push({ name: 'conversation', params: { id: data.id } })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateVirtualHeight() {
|
|
||||||
if (this.hide) return // no updates when not rendering
|
|
||||||
if (!this.status) return // not loaded yet
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.virtualHeight = this.$refs.body.getBoundingClientRect().height
|
|
||||||
this.$emit('update:virtualHeight', {
|
|
||||||
id: this.status.id,
|
|
||||||
height: this.virtualHeight,
|
|
||||||
top: this.$el.clientTop,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default conversation
|
return {
|
||||||
|
...hoisted,
|
||||||
|
isLinearView,
|
||||||
|
isTreeView,
|
||||||
|
isExpanded,
|
||||||
|
conversation,
|
||||||
|
hide,
|
||||||
|
collapsable,
|
||||||
|
mobileLayout,
|
||||||
|
toggleExpanded,
|
||||||
|
isPage,
|
||||||
|
status,
|
||||||
|
loadStatusError,
|
||||||
|
maybeFocused,
|
||||||
|
setFocused,
|
||||||
|
showOtherRepliesButtonBelowStatus,
|
||||||
|
showOtherRepliesButtonInsideStatus,
|
||||||
|
onStatusSuspendStateChange,
|
||||||
|
getStatusClasses,
|
||||||
|
getReplies,
|
||||||
|
currentAncestors,
|
||||||
|
statusId,
|
||||||
|
collapsable,
|
||||||
|
pinnedStatusIdsObject,
|
||||||
|
inProfile,
|
||||||
|
profileUserId,
|
||||||
|
virtualHidden,
|
||||||
|
shouldShowAncestors,
|
||||||
|
shouldShowAllConversationButton,
|
||||||
|
topLevel,
|
||||||
|
shouldFadeAncestors,
|
||||||
|
currentStatus,
|
||||||
|
treeViewIsSimple,
|
||||||
|
diveToTopLevel,
|
||||||
|
threadDisplay,
|
||||||
|
threadDisplayDefault,
|
||||||
|
replies,
|
||||||
|
diveIntoStatus,
|
||||||
|
showThreadRecursively,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
</template>
|
</template>
|
||||||
<template #text>
|
<template #text>
|
||||||
<span>
|
<span>
|
||||||
{{ $t('status.show_all_conversation', { numStatus: otherTopLevelCount }, otherTopLevelCount) }}
|
{{ $t('status.show_all_conversation', { numStatus: topLevel.length - 1 }, topLevel.length - 1) }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
|
|
@ -91,11 +91,11 @@
|
||||||
class="thread-ancestors"
|
class="thread-ancestors"
|
||||||
:min-item-size="15"
|
:min-item-size="15"
|
||||||
:buffer="500"
|
:buffer="500"
|
||||||
:items="ancestorsOf(diveRoot)"
|
:items="currentAncestors"
|
||||||
role="feed"
|
role="feed"
|
||||||
list-tag="article"
|
list-tag="article"
|
||||||
item-tag="article"
|
item-tag="article"
|
||||||
:item-class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors, 'thread-ancestor': true }"
|
:item-class="{'thread-ancestor-has-other-replies': getReplies(status.id).size > 1, '-faded': shouldFadeAncestors, 'thread-ancestor': true }"
|
||||||
flow-mode
|
flow-mode
|
||||||
page-mode
|
page-mode
|
||||||
>
|
>
|
||||||
|
|
@ -105,7 +105,6 @@
|
||||||
:active="active"
|
:active="active"
|
||||||
>
|
>
|
||||||
<Status
|
<Status
|
||||||
ref="statusComponent"
|
|
||||||
class="conversation-status panel-body"
|
class="conversation-status panel-body"
|
||||||
:class="getStatusClasses(status, active)"
|
:class="getStatusClasses(status, active)"
|
||||||
|
|
||||||
|
|
@ -119,17 +118,15 @@
|
||||||
:in-profile="inProfile"
|
:in-profile="inProfile"
|
||||||
:in-conversation="isExpanded"
|
:in-conversation="isExpanded"
|
||||||
:profile-user-id="profileUserId"
|
:profile-user-id="profileUserId"
|
||||||
:simple-tree="treeViewIsSimple"
|
|
||||||
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
||||||
can-dive
|
can-dive
|
||||||
|
|
||||||
@goto="setFocused"
|
@goto="setFocused"
|
||||||
@dive="() => diveIntoStatus(status.id)"
|
@dive="() => diveIntoStatus(status.id)"
|
||||||
@suspendable-state-change="onStatusSuspendStateChange"
|
@suspendable-state-change="onStatusSuspendStateChange"
|
||||||
@height-change="updateVirtualHeight"
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).size > 1"
|
||||||
class="thread-ancestor-dive-box"
|
class="thread-ancestor-dive-box"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -149,7 +146,7 @@
|
||||||
</template>
|
</template>
|
||||||
<template #text>
|
<template #text>
|
||||||
<span>
|
<span>
|
||||||
{{ $t('status.ancestor_follow', { numReplies: getReplies(status.id, getReplies(status.id).length - 1).length - 1 }) }}
|
{{ $t('status.ancestor_follow', { numReplies: getReplies(status.id, getReplies(status.id).size - 1).size - 1 }) }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
|
|
@ -159,12 +156,10 @@
|
||||||
</template>
|
</template>
|
||||||
</DynamicScroller>
|
</DynamicScroller>
|
||||||
<ThreadTree
|
<ThreadTree
|
||||||
v-for="status in showingTopLevel"
|
:key="currentStatus.id"
|
||||||
:key="status.id"
|
|
||||||
ref="statusComponent"
|
|
||||||
:depth="0"
|
:depth="0"
|
||||||
|
|
||||||
:status-id="status.id"
|
:status-id="currentStatus.id"
|
||||||
:in-profile="inProfile"
|
:in-profile="inProfile"
|
||||||
:conversation="conversation"
|
:conversation="conversation"
|
||||||
:collapsable="collapsable"
|
:collapsable="collapsable"
|
||||||
|
|
@ -172,21 +167,18 @@
|
||||||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||||
:profile-user-id="profileUserId"
|
:profile-user-id="profileUserId"
|
||||||
|
|
||||||
:get-replies="getReplies"
|
:replies="replies"
|
||||||
:focused="maybeFocused"
|
:focused="maybeFocused"
|
||||||
:toggle-expanded="toggleExpanded"
|
|
||||||
|
|
||||||
:simple="treeViewIsSimple"
|
:thread-display="threadDisplay"
|
||||||
:thread-display-status="threadDisplayStatus"
|
:thread-display-default="threadDisplayDefault"
|
||||||
:show-thread-recursively="showThreadRecursively"
|
:can-dive="isExpanded"
|
||||||
:total-reply-count="totalReplyCount"
|
|
||||||
:total-reply-depth="totalReplyDepth"
|
|
||||||
:can-dive="canDive"
|
|
||||||
|
|
||||||
@goto="setFocused"
|
@goto="setFocused"
|
||||||
@dive="diveIntoStatus"
|
@dive="diveIntoStatus"
|
||||||
|
@toggle-expanded="toggleExpanded"
|
||||||
|
@show-thread-recursively="showThreadRecursively"
|
||||||
@suspendable-state-change="onStatusSuspendStateChange"
|
@suspendable-state-change="onStatusSuspendStateChange"
|
||||||
@height-change="updateVirtualHeight"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DynamicScroller
|
<DynamicScroller
|
||||||
|
|
@ -208,7 +200,6 @@
|
||||||
>
|
>
|
||||||
<Status
|
<Status
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
ref="statusComponent"
|
|
||||||
class="conversation-status"
|
class="conversation-status"
|
||||||
:class="getStatusClasses(status, active)"
|
:class="getStatusClasses(status, active)"
|
||||||
:status-id="status.id"
|
:status-id="status.id"
|
||||||
|
|
@ -225,7 +216,6 @@
|
||||||
@goto="setFocused"
|
@goto="setFocused"
|
||||||
@toggle-expanded="toggleExpanded"
|
@toggle-expanded="toggleExpanded"
|
||||||
@suspendable-state-change="onStatusSuspendStateChange"
|
@suspendable-state-change="onStatusSuspendStateChange"
|
||||||
@height-change="updateVirtualHeight"
|
|
||||||
/>
|
/>
|
||||||
</DynamicScrollerItem>
|
</DynamicScrollerItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<template #statusLink>
|
<template #statusLink>
|
||||||
<router-link
|
<router-link
|
||||||
class="faint-link"
|
class="faint-link"
|
||||||
:to="{ name: 'conversation', params: { id: draft.refId } }"
|
:to="{ name: 'conversation', params: { statusId: draft.refId } }"
|
||||||
>
|
>
|
||||||
{{ refStatus ? refStatus.external_url : $t('drafts.unavailable') }}
|
{{ refStatus ? refStatus.external_url : $t('drafts.unavailable') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="notification.status"
|
v-if="notification.status"
|
||||||
:to="{ name: 'conversation', params: { id: notification.status.id } }"
|
:to="{ name: 'conversation', params: { statusId: notification.status.id } }"
|
||||||
class="timeago-link faint"
|
class="timeago-link faint"
|
||||||
>
|
>
|
||||||
<Timeago
|
<Timeago
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
<router-link
|
<router-link
|
||||||
v-for="status in report.statuses"
|
v-for="status in report.statuses"
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
:to="{ name: 'conversation', params: { id: status.id } }"
|
:to="{ name: 'conversation', params: { statusId: status.id } }"
|
||||||
class="reported-status"
|
class="reported-status"
|
||||||
>
|
>
|
||||||
<div class="reported-status-heading">
|
<div class="reported-status-heading">
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ const Status = {
|
||||||
props: {
|
props: {
|
||||||
statusId: String,
|
statusId: String,
|
||||||
statusoid: Object,
|
statusoid: Object,
|
||||||
replies: Array,
|
replies: Set,
|
||||||
|
|
||||||
expandable: Boolean,
|
expandable: Boolean,
|
||||||
focused: Boolean,
|
focused: Boolean,
|
||||||
|
|
@ -109,12 +109,11 @@ const Status = {
|
||||||
inQuote: Boolean,
|
inQuote: Boolean,
|
||||||
|
|
||||||
profileUserId: String,
|
profileUserId: String,
|
||||||
simpleTree: Boolean,
|
|
||||||
showOtherRepliesAsButton: Boolean,
|
showOtherRepliesAsButton: Boolean,
|
||||||
canDive: Boolean,
|
canDive: Boolean,
|
||||||
ignoreMute: Boolean,
|
ignoreMute: Boolean,
|
||||||
|
|
||||||
threadDisplayStatus: String,
|
threadDisplayState: String,
|
||||||
},
|
},
|
||||||
emits: [
|
emits: [
|
||||||
'goto',
|
'goto',
|
||||||
|
|
@ -166,6 +165,9 @@ const Status = {
|
||||||
user() {
|
user() {
|
||||||
return useUsersStore().findUser(this.mainStatus.user.id)
|
return useUsersStore().findUser(this.mainStatus.user.id)
|
||||||
},
|
},
|
||||||
|
simpleTree() {
|
||||||
|
return !this.mergedConfig.conversationTreeAdvanced
|
||||||
|
},
|
||||||
showReasonMutedThread() {
|
showReasonMutedThread() {
|
||||||
return (
|
return (
|
||||||
(this.mainStatus.thread_muted || this.repeatStatus?.thread_muted) &&
|
(this.mainStatus.thread_muted || this.repeatStatus?.thread_muted) &&
|
||||||
|
|
@ -436,10 +438,10 @@ const Status = {
|
||||||
return !this.replying && this.mediaPlaying.size === 0
|
return !this.replying && this.mediaPlaying.size === 0
|
||||||
},
|
},
|
||||||
inThreadForest() {
|
inThreadForest() {
|
||||||
return !!this.threadDisplayStatus
|
return !!this.threadDisplayState
|
||||||
},
|
},
|
||||||
threadShowing() {
|
threadShowing() {
|
||||||
return this.threadDisplayStatus === 'showing'
|
return this.threadDisplayState === 'showing'
|
||||||
},
|
},
|
||||||
visibilityLocalized() {
|
visibilityLocalized() {
|
||||||
return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility)
|
return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility)
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,7 @@
|
||||||
</span>
|
</span>
|
||||||
<router-link
|
<router-link
|
||||||
class="timeago faint"
|
class="timeago faint"
|
||||||
:to="{ name: 'conversation', params: { id: status.id } }"
|
:to="{ name: 'conversation', params: { statusId: status.id } }"
|
||||||
>
|
>
|
||||||
<Timeago
|
<Timeago
|
||||||
:time="mainStatus.created_at"
|
:time="mainStatus.created_at"
|
||||||
|
|
@ -222,7 +222,7 @@
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="inThreadForest && replies?.length && !simpleTree"
|
v-if="inThreadForest && replies?.size && !simpleTree"
|
||||||
class="button-unstyled"
|
class="button-unstyled"
|
||||||
:title="threadShowing ? $t('status.thread_hide') : $t('status.thread_show')"
|
:title="threadShowing ? $t('status.thread_hide') : $t('status.thread_show')"
|
||||||
:aria-expanded="threadShowing ? 'true' : 'false'"
|
:aria-expanded="threadShowing ? 'true' : 'false'"
|
||||||
|
|
@ -423,16 +423,16 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="inConversation && !isPreview && replies?.length"
|
v-if="inConversation && !isPreview && replies?.size"
|
||||||
class="replies"
|
class="replies"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="showOtherRepliesAsButton && replies.length > 1"
|
v-if="showOtherRepliesAsButton && replies.size > 1"
|
||||||
class="button-unstyled -link"
|
class="button-unstyled -link"
|
||||||
:title="$t('status.ancestor_follow', { numReplies: replies.length - 1 }, replies.length - 1)"
|
:title="$t('status.ancestor_follow', { numReplies: replies.size - 1 }, replies.size - 1)"
|
||||||
@click.prevent="$emit('dive')"
|
@click.prevent="$emit('dive')"
|
||||||
>
|
>
|
||||||
{{ $t('status.replies_list_with_others', { numReplies: replies.length - 1 }, replies.length - 1) }}
|
{{ $t('status.replies_list_with_others', { numReplies: replies.size - 1 }, replies.size - 1) }}
|
||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
|
|
@ -441,7 +441,7 @@
|
||||||
{{ $t('status.replies_list') }}
|
{{ $t('status.replies_list') }}
|
||||||
</span>
|
</span>
|
||||||
<StatusPopover
|
<StatusPopover
|
||||||
v-for="reply in replies"
|
v-for="reply in replies.values()"
|
||||||
:key="reply.id"
|
:key="reply.id"
|
||||||
:status-id="reply.id"
|
:status-id="reply.id"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@ export const BUTTONS = [
|
||||||
return chatView
|
return chatView
|
||||||
},
|
},
|
||||||
action({ router, status }) {
|
action({ router, status }) {
|
||||||
router.push({ name: 'conversation', params: { id: status.id } })
|
router.push({ name: 'conversation', params: { statusId: status.id } })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -293,7 +293,7 @@ export const BUTTONS = [
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
[
|
[
|
||||||
useInstanceStore().server,
|
useInstanceStore().server,
|
||||||
router.resolve({ name: 'conversation', params: { id: status.id } })
|
router.resolve({ name: 'conversation', params: { statusId: status.id } })
|
||||||
.href,
|
.href,
|
||||||
].join(''),
|
].join(''),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,41 +4,95 @@ import {
|
||||||
faAngleDoubleRight,
|
faAngleDoubleRight,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
|
||||||
library.add(faAngleDoubleDown, faAngleDoubleRight)
|
library.add(faAngleDoubleDown, faAngleDoubleRight)
|
||||||
|
|
||||||
const ThreadTree = {
|
const ThreadTree = {
|
||||||
components: {},
|
components: {},
|
||||||
name: 'ThreadTree',
|
name: 'ThreadTree',
|
||||||
props: {
|
props: {
|
||||||
depth: Number,
|
|
||||||
statusId: String,
|
statusId: String,
|
||||||
inProfile: Boolean,
|
inProfile: Boolean,
|
||||||
conversation: Array,
|
|
||||||
collapsable: Boolean,
|
collapsable: Boolean,
|
||||||
isExpanded: Boolean,
|
isExpanded: Boolean,
|
||||||
pinnedStatusIdsObject: Object,
|
pinnedStatusIdsObject: Object,
|
||||||
profileUserId: String,
|
profileUserId: String,
|
||||||
|
|
||||||
|
depth: Number,
|
||||||
|
conversation: Array,
|
||||||
focused: String,
|
focused: String,
|
||||||
getReplies: Function,
|
replies: Map,
|
||||||
toggleExpanded: Function,
|
|
||||||
|
|
||||||
simple: Boolean,
|
|
||||||
canDive: Boolean,
|
canDive: Boolean,
|
||||||
threadDisplayStatus: Object,
|
threadDisplay: Map,
|
||||||
showThreadRecursively: Function,
|
threadDisplayDefault: Map,
|
||||||
totalReplyCount: Object,
|
|
||||||
totalReplyDepth: Object,
|
|
||||||
},
|
},
|
||||||
emits: ['suspendableStateChange', 'goto', 'dive', 'heightChange'],
|
emits: [
|
||||||
|
'suspendableStateChange',
|
||||||
|
'goto',
|
||||||
|
'dive',
|
||||||
|
'heightChange',
|
||||||
|
'toggleExpanded',
|
||||||
|
'showThreadRecursively',
|
||||||
|
],
|
||||||
computed: {
|
computed: {
|
||||||
currentReplies() {
|
currentReplies() {
|
||||||
return this.getReplies(this.statusId).map(({ id }) => id)
|
return [...this.getReplies(this.statusId)].map(({ id }) => id)
|
||||||
|
},
|
||||||
|
simple() {
|
||||||
|
return !useMergedConfigStore().mergedConfig.conversationTreeAdvanced
|
||||||
},
|
},
|
||||||
threadShowing() {
|
threadShowing() {
|
||||||
return this.threadDisplayStatus[this.statusId] === 'showing'
|
const result = this.threadDisplay.get(this.statusId) ?? this.threadDisplayDefault.get(this.statusId)
|
||||||
|
return result === 'showing'
|
||||||
|
},
|
||||||
|
totalReplyCount() {
|
||||||
|
const sizes = {}
|
||||||
|
const subTreeSizeFor = (id) => {
|
||||||
|
if (sizes[id]) {
|
||||||
|
return sizes[id]
|
||||||
|
}
|
||||||
|
sizes[id] =
|
||||||
|
1 +
|
||||||
|
[...this.getReplies(id)]
|
||||||
|
.map(({ id }) => id)
|
||||||
|
.map((cid) => subTreeSizeFor(cid))
|
||||||
|
.reduce((a, b) => a + b, 0)
|
||||||
|
return sizes[id]
|
||||||
|
}
|
||||||
|
this.conversation.map((k) => k.id).forEach(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.getReplies(id)]
|
||||||
|
.map(({ id }) => id)
|
||||||
|
.map((cid) => subTreeDepthFor(cid))
|
||||||
|
.reduce((a, b) => (a > b ? a : b), 0)
|
||||||
|
return depths[id]
|
||||||
|
}
|
||||||
|
this.conversation.map((k) => k.id).forEach(subTreeDepthFor)
|
||||||
|
return Object.keys(depths).reduce((res, id) => {
|
||||||
|
res[id] = depths[id] - 1 // exclude itself
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
getReplies(id) {
|
||||||
|
return this.replies.get(id) ?? new Set()
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ThreadTree
|
export default ThreadTree
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,14 @@
|
||||||
:profile-user-id="profileUserId"
|
:profile-user-id="profileUserId"
|
||||||
class="conversation-status conversation-status-treeview panel-body"
|
class="conversation-status conversation-status-treeview panel-body"
|
||||||
|
|
||||||
:simple-tree="simple"
|
:thread-display-state="threadDisplay.get(statusId)"
|
||||||
:thread-display-status="threadDisplayStatus[statusId]"
|
|
||||||
:can-dive="canDive"
|
:can-dive="canDive"
|
||||||
|
|
||||||
@dive="$emit('dive', statusId)"
|
@dive="$emit('dive', statusId)"
|
||||||
@goto="$emit('goto', statusId)"
|
@goto="$emit('goto', statusId)"
|
||||||
@toggle-expanded="toggleExpanded"
|
@toggle-expanded="$emit('toggleExpanded', statusId)"
|
||||||
@suspendable-state-change="e => $emit('suspendableStateChange', e)"
|
@suspendable-state-change="$emit('suspendableStateChange', e)"
|
||||||
@height-change="e => $emit('heightChange', e)"
|
@height-change="$emit('heightChange', e)"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="currentReplies.length > 0 && threadShowing"
|
v-if="currentReplies.length > 0 && threadShowing"
|
||||||
|
|
@ -42,20 +41,18 @@
|
||||||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||||
:profile-user-id="profileUserId"
|
:profile-user-id="profileUserId"
|
||||||
|
|
||||||
:get-replies="getReplies"
|
:replies="replies"
|
||||||
:focused="focused"
|
:focused="focused"
|
||||||
:toggle-expanded="toggleExpanded"
|
|
||||||
|
|
||||||
:simple="simple"
|
:thread-display="threadDisplay"
|
||||||
:thread-display-status="threadDisplayStatus"
|
:thread-display-default="threadDisplayDefault"
|
||||||
:show-thread-recursively="showThreadRecursively"
|
|
||||||
:total-reply-count="totalReplyCount"
|
|
||||||
:total-reply-depth="totalReplyDepth"
|
|
||||||
|
|
||||||
:can-dive="canDive"
|
:can-dive="canDive"
|
||||||
|
@show-thread-recursively="(e) => $emit('showThreadRecursively', e)"
|
||||||
@goto="(e) => $emit('goto', e)"
|
@goto="(e) => $emit('goto', e)"
|
||||||
@dive="(e) => $emit('dive', e)"
|
@dive="(e) => $emit('dive', e)"
|
||||||
@suspendable-state-change="e => $emit('suspendableStateChange', e)"
|
@suspendable-state-change="e => $emit('suspendableStateChange', e)"
|
||||||
|
@toggle-expanded="(e) => $emit('toggleExpanded', e)"
|
||||||
@height-change="e => $emit('heightChange', e)"
|
@height-change="e => $emit('heightChange', e)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -88,7 +85,7 @@
|
||||||
tag="button"
|
tag="button"
|
||||||
keypath="status.thread_show_full_with_icon"
|
keypath="status.thread_show_full_with_icon"
|
||||||
class="button-unstyled -link thread-tree-show-replies-button"
|
class="button-unstyled -link thread-tree-show-replies-button"
|
||||||
@click.prevent="showThreadRecursively(statusId)"
|
@click.prevent="$emit('showThreadRecursively', statusId)"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue