big cleanup, lint, relying on provide/inject instead of drilling props

This commit is contained in:
Henry Jameson 2026-09-08 16:11:08 +03:00
commit 6ef8b36c73
10 changed files with 178 additions and 262 deletions

View file

@ -1,6 +1,6 @@
import { get, reduce } from 'lodash-es' import { get } from 'lodash-es'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { computed, nextTick, ref, watch, toRefs } from 'vue' import { computed, nextTick, provide, ref, toRefs, watch } from 'vue'
import { useRouter } from 'vue-router' 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'
@ -16,7 +16,10 @@ 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 as apiFetchConversation, 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'
@ -36,24 +39,6 @@ library.add(
faTimes, faTimes,
) )
const sortById = (a, b) => {
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
const idB = b.type === 'retweet' ? b.retweeted_status.id : b.id
const seqA = Number(idA)
const seqB = Number(idB)
const isSeqA = !Number.isNaN(seqA)
const isSeqB = !Number.isNaN(seqB)
if (isSeqA && isSeqB) {
return seqA < seqB ? -1 : 1
} else if (isSeqA && !isSeqB) {
return -1
} else if (!isSeqA && isSeqB) {
return 1
} else {
return idA < idB ? -1 : 1
}
}
export default { export default {
props: { props: {
statusId: { statusId: {
@ -61,34 +46,12 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
collapsable: {
// Whether conversation can be collapsed
// i.e. when it's not a page
type: Boolean,
default: false,
},
isPage: { isPage: {
// Whether conversation is rendered as a standalone page // Whether conversation is rendered as a standalone page
// as opposed to embedded into a timeline // as opposed to embedded into a timeline
type: Boolean, type: Boolean,
default: false, default: false,
}, },
pinnedStatusIdsObject: {
// Used for user profile, map of pinned statuses
type: Object,
default: null,
},
inProfile: {
// Whether conversation is rendered in a user profile
// used for overriding muted status
type: Boolean,
default: false,
},
profileUserId: {
// used with inProfile, user id of the profile
type: String,
default: null,
},
virtualHidden: { virtualHidden: {
// Whether conversation is suspended. Controls rendering of statuses // Whether conversation is suspended. Controls rendering of statuses
type: Boolean, type: Boolean,
@ -106,25 +69,18 @@ export default {
}, },
setup(props, ctx) { setup(props, ctx) {
const { emit } = ctx const { emit } = ctx
const { const { statusId } = toRefs(props)
statusId,
collapsable,
pinnedStatusIdsObject,
inProfile,
profileUserId,
virtualHidden,
} = toRefs(props)
const router = useRouter()
const hoisted = {} const router = useRouter()
// # Main Configuration // # Main Configuration
const { mergedConfig } = storeToRefs(useMergedConfigStore()) const { mergedConfig } = storeToRefs(useMergedConfigStore())
const { mastoUserSocketStatus } = storeToRefs(useStreamingStore()) const { mastoUserSocketStatus } = storeToRefs(useStreamingStore())
const displayStyle = computed(() => mergedConfig.value.conversationDisplay) const displayStyle = computed(() => mergedConfig.value.conversationDisplay)
const streamingEnabled = computed(() => const streamingEnabled = computed(
mergedConfig.value.useStreamingApi && () =>
mastoUserSocketStatus === WSConnectionStatus.JOINED mergedConfig.value.useStreamingApi &&
mastoUserSocketStatus === WSConnectionStatus.JOINED,
) )
// # Main things // # Main things
@ -146,6 +102,24 @@ export default {
return statusId.value return statusId.value
} }
}) })
const sortById = (a, b) => {
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
const idB = b.type === 'retweet' ? b.retweeted_status.id : b.id
const seqA = Number(idA)
const seqB = Number(idB)
const isSeqA = !Number.isNaN(seqA)
const isSeqB = !Number.isNaN(seqB)
if (isSeqA && isSeqB) {
return seqA < seqB ? -1 : 1
} else if (isSeqA && !isSeqB) {
return -1
} else if (!isSeqA && isSeqB) {
return 1
} else {
return idA < idB ? -1 : 1
}
}
const conversationId = computed(() => getConversationId(statusId.value)) const conversationId = computed(() => getConversationId(statusId.value))
const conversation = computed(() => { const conversation = computed(() => {
if (!status.value) { if (!status.value) {
@ -167,11 +141,7 @@ export default {
}) })
const replies = computed(() => const replies = computed(() =>
conversation.value.reduce( conversation.value.reduce(
( (result, { id, in_reply_to_status_id: irid }, index) => {
result,
{ id, in_reply_to_status_id: irid },
index,
) => {
if (irid) { if (irid) {
if (!result.has(irid)) { if (!result.has(irid)) {
result.set(irid, new Set()) result.set(irid, new Set())
@ -184,18 +154,18 @@ export default {
return result return result
}, },
new Map(), new Map(),
) ),
) )
const getReplies = (id) => replies.value.get(id) ?? new Set() const getReplies = (id) => replies.value.get(id) ?? new Set()
provide('conversation', conversation)
provide('replies', replies)
const fetchConversation = async () => { const fetchConversation = async () => {
if (status.value) { if (status.value) {
const { const {
data: { data: { ancestors, descendants },
ancestors, timestamp,
descendants
},
timestamp
} = await apiFetchConversation({ } = await apiFetchConversation({
id: statusId.value, id: statusId.value,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
@ -225,11 +195,12 @@ export default {
} }
} }
const resetDisplayState = () => { const resetDisplayState = () => {
hoisted.undive() setFocused(statusId.value)
threadDisplay.value = new Map() threadDisplay.value = new Map()
} }
// # Virtual scrolling stuff // # Virtual scrolling stuff
const { virtualHidden } = toRefs(props)
const unsuspendibleIds = ref(new Set()) const unsuspendibleIds = ref(new Set())
const suspendable = computed(() => unsuspendibleIds.value.size === 0) const suspendable = computed(() => unsuspendibleIds.value.size === 0)
const hide = computed(() => virtualHidden.value && suspendable.value) const hide = computed(() => virtualHidden.value && suspendable.value)
@ -243,13 +214,13 @@ export default {
// # Misc UI things // # Misc UI things
const loadStatusError = ref(null) const loadStatusError = ref(null)
const { layoutType } = storeToRefs(useInterfaceStore()) const { layoutType } = storeToRefs(useInterfaceStore())
const mobileLayout = computed(() => layoutType.value === 'mobile') const mobileLayout = computed(() => layoutType.value === 'mobile')
const firstStatus = computed(() => conversation.value[0]) const firstStatus = computed(() => conversation.value[0])
const lastStatus = computed(() => conversation.value[conversation.value.legnth - 1]) const lastStatus = computed(
() => conversation.value[conversation.value.legnth - 1],
)
const getStatusClasses = (status, active) => ({ const getStatusClasses = (status, active) => ({
'-virtual-active': active,
'-first': status.id === firstStatus.value?.id, '-first': status.id === firstStatus.value?.id,
'-last': status.id === lastStatus.value?.id, '-last': status.id === lastStatus.value?.id,
}) })
@ -268,13 +239,15 @@ export default {
resetDisplayState() resetDisplayState()
} }
}) })
provide('isExpanded', isExpanded)
provide('isPage', isPage)
// # Focus // # Focus
const focused = ref(null) const focusedId = ref(statusId.value)
const maybeFocused = computed(() => isExpanded.value ? focused.value : null) const focused = computed(() => (isExpanded.value ? focusedId.value : null))
const setFocused = (id) => { const setFocused = (id) => {
if (!id) return if (!id) return
focused.value = id focusedId.value = id
if (!streamingEnabled.value) { if (!streamingEnabled.value) {
useStatusesStore().fetchStatus(id) useStatusesStore().fetchStatus(id)
@ -296,6 +269,7 @@ export default {
fetchConversation() fetchConversation()
} }
}) })
provide('focused', focused)
// Component created // Component created
if (isPage.value) { if (isPage.value) {
@ -309,11 +283,15 @@ export default {
const isTreeView = computed(() => displayStyle.value === 'tree') const isTreeView = computed(() => displayStyle.value === 'tree')
// ## Tree view settings // ## Tree view settings
const treeViewIsSimple = computed(() => !mergedConfig.value.conversationTreeAdvanced) const treeViewIsSimple = computed(
const shouldFadeAncestors = computed(() => mergedConfig.value.conversationTreeFadeAncestors) () => !mergedConfig.value.conversationTreeAdvanced,
const otherRepliesButtonPosition = computed(() => mergedConfig.value.conversationOtherRepliesButton) )
const showOtherRepliesButtonBelowStatus = computed(() => otherRepliesButtonPosition.value === 'below') const shouldFadeAncestors = computed(
const showOtherRepliesButtonInsideStatus = computed(() => otherRepliesButtonPosition.value === 'inside') () => mergedConfig.value.conversationTreeFadeAncestors,
)
const showOtherRepliesButtonBelowStatus = computed(
() => mergedConfig.value.conversationOtherRepliesButton === 'below',
)
const maxDepthToShowByDefault = computed(() => { const maxDepthToShowByDefault = computed(() => {
// maxDepthInThread = max number of depths that is *visible* // maxDepthInThread = max number of depths that is *visible*
// since our depth starts with 0 and "showing" means "showing children" // since our depth starts with 0 and "showing" means "showing children"
@ -323,19 +301,12 @@ export default {
}) })
// ## Tree style state // ## Tree style state
// ### Dive
const inlineDivePosition = ref(null)
const currentStatusId = computed(() => inlineDivePosition.value ?? statusId.value)
// ### Topology // ### Topology
const ancestors = computed(() => { const ancestors = computed(() => {
// First we fill map with empty sets and add given id's parent // First we fill map with empty sets and add given id's parent
// as set's only element (if any) // as set's only element (if any)
const parentMap = conversation.value.reduce( const parentMap = conversation.value.reduce(
( (result, { id, in_reply_to_status_id: irid }) => {
result,
{ id, in_reply_to_status_id: irid },
) => {
if (!result.has(id)) { if (!result.has(id)) {
result.set(id, new Set()) result.set(id, new Set())
} }
@ -345,7 +316,7 @@ export default {
} }
return result return result
}, },
new Map() new Map(),
) )
// Next we iterate over each entry and fill the whole ancestry chain // Next we iterate over each entry and fill the whole ancestry chain
@ -361,15 +332,19 @@ export default {
}) })
return parentMap return parentMap
}) })
const getAncestorIds = (id) => ancestors.value.get(id) ?? new Set() const topLevel = computed(() =>
const getAncestors = (id) => [...getAncestorIds(id)].map(getStatusObject).filter(Boolean) [...ancestors.value.entries()]
const currentAncestors = computed(() => getAncestors(currentStatusId.value).reverse()) .filter(([id, ancestors]) => ancestors.size === 0)
const currentDepth = computed(() => currentAncestors.value.length) .map(([id]) => getStatusObject(id)),
const topLevel = computed(() => [...ancestors.value.entries()]
.filter(([id, ancestors]) => ancestors.size === 0)
.map(([id]) => getStatusObject(id))
) )
const currentStatus = computed(() => getStatusObject(currentStatusId.value)) const getAncestorIds = (id) => ancestors.value.get(id) ?? new Set()
const getAncestors = (id) =>
[...getAncestorIds(id)].map(getStatusObject).filter(Boolean)
const currentAncestors = computed(() =>
getAncestors(focusedId.value).reverse(),
)
const currentDepth = computed(() => currentAncestors.value.length)
const currentStatus = computed(() => getStatusObject(focusedId.value))
// ### Thread Display // ### Thread Display
const threadDisplay = ref(new Map()) // id => 'showing' | 'hidden' const threadDisplay = ref(new Map()) // id => 'showing' | 'hidden'
@ -390,6 +365,8 @@ export default {
return map return map
}, new Map()) }, new Map())
}) })
provide('threadDisplay', threadDisplay)
provide('threadDisplayDefault', threadDisplayDefault)
const setThreadDisplayRecursively = (id, value) => { const setThreadDisplayRecursively = (id, value) => {
threadDisplay.value.set(id, value) threadDisplay.value.set(id, value)
@ -402,8 +379,12 @@ export default {
} }
// ## Derived values // ## Derived values
const shouldShowAllConversationButton = computed(() => currentAncestors.value.length > 0 && topLevel.value.length > 1) const shouldShowAllConversationButton = computed(
const shouldShowAncestors = computed(() => isExpanded.value && ancestors.value.get(currentStatusId.value) != null) () => currentAncestors.value.length > 0 && topLevel.value.length > 1,
)
const shouldShowAncestors = computed(
() => isExpanded.value && ancestors.value.get(focusedId.value) != null,
)
// # Scrolling / diving // # Scrolling / diving
const tryScrollTo = (id) => { const tryScrollTo = (id) => {
@ -412,8 +393,6 @@ export default {
} }
if (isPage.value) { if (isPage.value) {
router.push({ name: 'conversation', params: { statusId: id } }) router.push({ name: 'conversation', params: { statusId: id } })
} else {
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,
@ -434,47 +413,32 @@ export default {
setFocused(id) setFocused(id)
}) })
} }
const goToCurrent = () => {
tryScrollTo(diveRoot)
}
const diveIntoStatus = (id) => { const diveIntoStatus = (id) => {
tryScrollTo(id) tryScrollTo(id)
} }
const diveToTopLevel = () => { const diveToTopLevel = () => {
tryScrollTo(currentAncestors.value[0].id) tryScrollTo(currentAncestors.value[0].id)
} }
const undive = () => {
inlineDivePosition.value = null
setFocused(statusId.value)
}
hoisted.undive = undive
return { return {
...hoisted, hide,
isLinearView, isLinearView,
isTreeView, isTreeView,
isExpanded, isExpanded,
conversation, conversation,
hide,
collapsable,
mobileLayout, mobileLayout,
toggleExpanded, toggleExpanded,
isPage, isPage,
status, status,
loadStatusError, loadStatusError,
maybeFocused, focused,
setFocused, setFocused,
showOtherRepliesButtonBelowStatus, showOtherRepliesButtonBelowStatus,
showOtherRepliesButtonInsideStatus,
onStatusSuspendStateChange, onStatusSuspendStateChange,
getStatusClasses, getStatusClasses,
getReplies, getReplies,
currentAncestors, currentAncestors,
statusId, statusId,
collapsable,
pinnedStatusIdsObject,
inProfile,
profileUserId,
virtualHidden, virtualHidden,
shouldShowAncestors, shouldShowAncestors,
shouldShowAllConversationButton, shouldShowAllConversationButton,
@ -489,5 +453,5 @@ export default {
diveIntoStatus, diveIntoStatus,
showThreadRecursively, showThreadRecursively,
} }
} },
} }

View file

@ -19,19 +19,19 @@
</template> </template>
</h1> </h1>
<button <button
v-if="collapsable" v-if="!isPage"
class="button-unstyled -link" class="button-unstyled -link"
@click.prevent="toggleExpanded" @click.prevent="toggleExpanded"
> >
{{ $t('timeline.collapse') }} {{ $t('timeline.collapse') }}
</button> </button>
<QuickFilterSettings <QuickFilterSettings
v-if="!collapsable && mobileLayout" v-if="isPage && mobileLayout"
:conversation="true" :conversation="true"
class="rightside-button" class="rightside-button"
/> />
<QuickViewSettings <QuickViewSettings
v-if="!collapsable" v-if="isPage"
:conversation="true" :conversation="true"
class="rightside-button" class="rightside-button"
/> />
@ -86,93 +86,61 @@
</template> </template>
</i18n-t> </i18n-t>
</div> </div>
<DynamicScroller <div
v-if="shouldShowAncestors" v-if="shouldShowAncestors"
class="thread-ancestors" class="thread-ancestors"
:min-item-size="15"
:buffer="500"
:items="currentAncestors"
role="feed"
list-tag="article"
item-tag="article"
:item-class="{'thread-ancestor-has-other-replies': getReplies(status.id).size > 1, '-faded': shouldFadeAncestors, 'thread-ancestor': true }"
flow-mode
page-mode
> >
<template #default="{ item: status, active }"> <article
<DynamicScrollerItem v-for="status in currentAncestors"
:item="status" class="thread-ancestor"
:active="active" :class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}"
>
<Status
class="conversation-status panel-body"
:class="getStatusClasses(status)"
:status-id="status.id"
:replies="getReplies(status.id)"
:focused="focused === status.id"
can-dive
@goto="setFocused"
@dive="() => diveIntoStatus(status.id)"
@suspendable-state-change="onStatusSuspendStateChange"
/>
<div
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).size > 1"
class="thread-ancestor-dive-box"
> >
<Status
class="conversation-status panel-body"
:class="getStatusClasses(status, active)"
:status-id="status.id"
:replies="getReplies(status.id)"
:expandable="!isExpanded"
:focused="maybeFocused === status.id"
:inline-expanded="collapsable && isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
:in-profile="inProfile"
:in-conversation="isExpanded"
:profile-user-id="profileUserId"
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
can-dive
@goto="setFocused"
@dive="() => diveIntoStatus(status.id)"
@suspendable-state-change="onStatusSuspendStateChange"
/>
<div <div
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).size > 1" class="thread-ancestor-dive-box-inner"
class="thread-ancestor-dive-box"
> >
<div <i18n-t
class="thread-ancestor-dive-box-inner" tag="button"
scope="global"
keypath="status.ancestor_follow_with_icon"
class="button-unstyled -link thread-tree-show-replies-button"
@click.prevent="diveIntoStatus(status.id)"
> >
<i18n-t <template #icon>
tag="button" <FAIcon
scope="global" icon="angle-double-right"
keypath="status.ancestor_follow_with_icon" />
class="button-unstyled -link thread-tree-show-replies-button" </template>
@click.prevent="diveIntoStatus(status.id)" <template #text>
> <span>
<template #icon> {{ $t('status.ancestor_follow', { numReplies: getReplies(status.id, getReplies(status.id).size - 1).size - 1 }) }}
<FAIcon </span>
icon="angle-double-right" </template>
/> </i18n-t>
</template>
<template #text>
<span>
{{ $t('status.ancestor_follow', { numReplies: getReplies(status.id, getReplies(status.id).size - 1).size - 1 }) }}
</span>
</template>
</i18n-t>
</div>
</div> </div>
</DynamicScrollerItem> </div>
</template> </article>
</DynamicScroller> </div>
<ThreadTree <ThreadTree
:key="currentStatus.id"
:depth="0"
:status-id="currentStatus.id" :status-id="currentStatus.id"
:in-profile="inProfile" :depth="0"
:conversation="conversation"
:collapsable="collapsable"
:is-expanded="isExpanded"
:pinned-status-ids-object="pinnedStatusIdsObject"
:profile-user-id="profileUserId"
:replies="replies"
:focused="maybeFocused"
:thread-display="threadDisplay"
:thread-display-default="threadDisplayDefault"
:can-dive="isExpanded"
@goto="setFocused" @goto="setFocused"
@dive="diveIntoStatus" @dive="diveIntoStatus"
@ -201,17 +169,11 @@
<Status <Status
:key="status.id" :key="status.id"
class="conversation-status" class="conversation-status"
:class="getStatusClasses(status, active)" :class="getStatusClasses(status)"
:status-id="status.id" :status-id="status.id"
:replies="getReplies(status.id)" :replies="getReplies(status.id)"
:expandable="!isExpanded" :focused="focused === status.id || focused === status.retweeted_status?.id"
:focused="maybeFocused === status.id || maybeFocused === status.retweeted_status?.id"
:inline-expanded="collapsable && isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
:in-profile="inProfile"
:in-conversation="isExpanded"
:profile-user-id="profileUserId"
@goto="setFocused" @goto="setFocused"
@toggle-expanded="toggleExpanded" @toggle-expanded="toggleExpanded"

View file

@ -27,7 +27,7 @@
<Status <Status
v-if="shouldDisplayQuote" v-if="shouldDisplayQuote"
:statusoid="quotedStatus" :statusoid="quotedStatus"
:in-quote="true" in-quote
/> />
</article> </article>
<p <p

View file

@ -98,19 +98,12 @@ const Status = {
statusoid: Object, statusoid: Object,
replies: Set, replies: Set,
expandable: Boolean,
focused: Boolean, focused: Boolean,
compact: Boolean, compact: Boolean,
isPreview: Boolean, isPreview: Boolean,
noHeading: Boolean, noHeading: Boolean,
inlineExpanded: Boolean,
inProfile: Boolean,
inConversation: Boolean,
inQuote: Boolean, inQuote: Boolean,
profileUserId: String,
showOtherRepliesAsButton: Boolean,
canDive: Boolean,
ignoreMute: Boolean, ignoreMute: Boolean,
threadDisplayState: String, threadDisplayState: String,
@ -122,6 +115,11 @@ const Status = {
'suspendableStateChange', 'suspendableStateChange',
'heightChange', 'heightChange',
], ],
inject: {
profileUserId: { default: null },
isPage: { default: false },
isExpanded: { default: false },
},
data() { data() {
return { return {
replying: false, replying: false,
@ -139,6 +137,12 @@ const Status = {
status() { status() {
return this.statusoid ?? useStatusesStore().allStatuses.get(this.statusId) return this.statusoid ?? useStatusesStore().allStatuses.get(this.statusId)
}, },
inConversation() {
return this.isExpanded
},
inProfile() {
return this.profileUserId != null
},
// Status repeated // Status repeated
repeatedStatus() { repeatedStatus() {
if (this.status.retweeted_status === undefined) return undefined if (this.status.retweeted_status === undefined) return undefined
@ -168,6 +172,9 @@ const Status = {
simpleTree() { simpleTree() {
return !this.mergedConfig.conversationTreeAdvanced return !this.mergedConfig.conversationTreeAdvanced
}, },
showOtherRepliesAsButton() {
return this.mergedConfig.conversationOtherRepliesButton === 'inside'
},
showReasonMutedThread() { showReasonMutedThread() {
return ( return (
(this.mainStatus.thread_muted || this.repeatStatus?.thread_muted) && (this.mainStatus.thread_muted || this.repeatStatus?.thread_muted) &&

View file

@ -3,7 +3,7 @@
v-if="!hideStatus" v-if="!hideStatus"
ref="root" ref="root"
class="Status" class="Status"
:class="[{ '-focused': focused }, { '-conversation': inlineExpanded }]" :class="[{ '-focused': focused }, { '-conversation': !isPage && isExpanded }]"
> >
<div <div
v-if="error" v-if="error"
@ -199,7 +199,7 @@
/> />
</span> </span>
<button <button
v-if="expandable && !isPreview" v-if="!isExpanded && !isPreview"
class="button-unstyled" class="button-unstyled"
:title="$t('status.expand')" :title="$t('status.expand')"
@click.prevent="toggleExpanded" @click.prevent="toggleExpanded"
@ -235,7 +235,7 @@
/> />
</button> </button>
<button <button
v-if="canDive && !simpleTree" v-if="isExpanded && !simpleTree"
class="button-unstyled" class="button-unstyled"
:title="$t('status.show_only_conversation_under_this')" :title="$t('status.show_only_conversation_under_this')"
@click.prevent="$emit('dive')" @click.prevent="$emit('dive')"

View file

@ -293,8 +293,10 @@ export const BUTTONS = [
navigator.clipboard.writeText( navigator.clipboard.writeText(
[ [
useInstanceStore().server, useInstanceStore().server,
router.resolve({ name: 'conversation', params: { statusId: status.id } }) router.resolve({
.href, name: 'conversation',
params: { statusId: status.id },
}).href,
].join(''), ].join(''),
) )
return Promise.resolve() return Promise.resolve()

View file

@ -1,11 +1,11 @@
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faAngleDoubleDown, faAngleDoubleDown,
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 = {
@ -13,20 +13,7 @@ const ThreadTree = {
name: 'ThreadTree', name: 'ThreadTree',
props: { props: {
statusId: String, statusId: String,
inProfile: Boolean,
collapsable: Boolean,
isExpanded: Boolean,
pinnedStatusIdsObject: Object,
profileUserId: String,
depth: Number, depth: Number,
conversation: Array,
focused: String,
replies: Map,
canDive: Boolean,
threadDisplay: Map,
threadDisplayDefault: Map,
}, },
emits: [ emits: [
'suspendableStateChange', 'suspendableStateChange',
@ -36,6 +23,15 @@ const ThreadTree = {
'toggleExpanded', 'toggleExpanded',
'showThreadRecursively', 'showThreadRecursively',
], ],
inject: [
'conversation',
'focused',
'replies',
'threadDisplay',
'threadDisplayDefault',
'isExpanded',
'isPage',
],
computed: { computed: {
currentReplies() { currentReplies() {
return [...this.getReplies(this.statusId)].map(({ id }) => id) return [...this.getReplies(this.statusId)].map(({ id }) => id)
@ -44,9 +40,14 @@ const ThreadTree = {
return !useMergedConfigStore().mergedConfig.conversationTreeAdvanced return !useMergedConfigStore().mergedConfig.conversationTreeAdvanced
}, },
threadShowing() { threadShowing() {
const result = this.threadDisplay.get(this.statusId) ?? this.threadDisplayDefault.get(this.statusId) const result =
this.threadDisplay.get(this.statusId) ??
this.threadDisplayDefault.get(this.statusId)
return result === 'showing' return result === 'showing'
}, },
canDive() {
return this.isExpanded
},
totalReplyCount() { totalReplyCount() {
const sizes = {} const sizes = {}
const subTreeSizeFor = (id) => { const subTreeSizeFor = (id) => {
@ -92,7 +93,7 @@ const ThreadTree = {
getReplies(id) { getReplies(id) {
return this.replies.get(id) ?? new Set() return this.replies.get(id) ?? new Set()
}, },
} },
} }
export default ThreadTree export default ThreadTree

View file

@ -2,20 +2,12 @@
<article class="thread-tree"> <article class="thread-tree">
<Status <Status
:key="statusId" :key="statusId"
ref="statusComponent" class="conversation-status conversation-status-treeview panel-body"
:status-id="statusId" :status-id="statusId"
:replies="getReplies(statusId)" :replies="getReplies(statusId)"
:inline-expanded="collapsable && isExpanded"
:expandable="!isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
:in-conversation="isExpanded"
:focused="focused === statusId" :focused="focused === statusId"
:in-profile="inProfile"
:profile-user-id="profileUserId"
class="conversation-status conversation-status-treeview panel-body"
:thread-display-state="threadDisplay.get(statusId)" :thread-display-state="threadDisplay.get(statusId)"
:can-dive="canDive"
@dive="$emit('dive', statusId)" @dive="$emit('dive', statusId)"
@goto="$emit('goto', statusId)" @goto="$emit('goto', statusId)"
@ -30,24 +22,9 @@
<ThreadTree <ThreadTree
v-for="replyStatusId in currentReplies" v-for="replyStatusId in currentReplies"
:key="replyStatusId" :key="replyStatusId"
ref="childComponent"
:depth="depth + 1" :depth="depth + 1"
:status-id="replyStatusId" :status-id="replyStatusId"
:in-profile="inProfile"
:conversation="conversation"
:collapsable="collapsable"
:is-expanded="isExpanded"
:pinned-status-ids-object="pinnedStatusIdsObject"
:profile-user-id="profileUserId"
:replies="replies"
:focused="focused"
:thread-display="threadDisplay"
:thread-display-default="threadDisplayDefault"
:can-dive="canDive"
@show-thread-recursively="(e) => $emit('showThreadRecursively', e)" @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)"

View file

@ -41,6 +41,11 @@ const Timeline = {
blockingClicks: false, blockingClicks: false,
} }
}, },
provide() {
return {
profileUserId: this.inProfile && this.timelineRef.argument,
}
},
components: { components: {
ScrollTopButton, ScrollTopButton,
Conversation, Conversation,

View file

@ -96,8 +96,6 @@
:key="status.id" :key="status.id"
role="listitem" role="listitem"
:status-id="status.id" :status-id="status.id"
:in-profile="inProfile"
:profile-user-id="timelineRef.argument"
collapsable collapsable
/> />
</DynamicScrollerItem> </DynamicScrollerItem>