diff --git a/biome.json b/biome.json
index a03cd02b5..0eb1d5e21 100644
--- a/biome.json
+++ b/biome.json
@@ -140,7 +140,15 @@
":BLANK_LINE:",
[":PATH:", "src/stores/**"],
":BLANK_LINE:",
- [":PATH:", "src/**", "src/stores/**", "src/components/**"],
+ [":PATH:", "src/composables/**"],
+ ":BLANK_LINE:",
+ [
+ ":PATH:",
+ "src/**",
+ "src/stores/**",
+ "src/components/**",
+ "src/composables/**"
+ ],
":BLANK_LINE:",
"@fortawesome/fontawesome-svg-core",
"@fortawesome/*"
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 919017387..e707008d1 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -3,7 +3,6 @@ import { storeToRefs } from 'pinia'
import {
computed,
nextTick,
- onMounted,
provide,
ref,
toRefs,
@@ -13,8 +12,6 @@ import {
import { useRouter } from 'vue-router'
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
-import { useScrollPosition } from 'src/components/conversation/useScrollPosition.js'
-import { useWindowSize } from 'src/components/conversation/useWindowSize.js'
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filter_settings.vue'
import QuickViewSettings from 'src/components/quick_view_settings/quick_view_settings.vue'
@@ -23,14 +20,14 @@ import ThreadTree from 'src/components/thread_tree/thread_tree.vue'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
-import { useOAuthStore } from 'src/stores/oauth.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useStreamingStore } from 'src/stores/streaming.js'
-import {
- fetchConversation as apiFetchConversation,
- fetchStatus as apiFetchStatus,
-} from 'src/api/public.js'
+import { useConversation } from 'src/composables/useConversation.js'
+import { useTreeConversationTopology } from 'src/composables/useTreeConversationTopology.js'
+import { useVirtualScrolling } from 'src/composables/useVirtualScrolling.js'
+import { useScrollPosition } from 'src/composables/useScrollPosition.js'
+
import { WSConnectionStatus } from 'src/api/websocket.js'
import { library } from '@fortawesome/fontawesome-svg-core'
@@ -73,11 +70,35 @@ export default {
RichContent,
},
setup(props) {
+ // # Helpers
+ const getStatusObject = (id) => useStatusesStore().allStatuses.get(id)
+ const getConversationId = (statusId) => {
+ const status = getStatusObject(statusId)
+ return get(
+ status,
+ 'retweeted_status.statusnet_conversation_id',
+ get(status, 'statusnet_conversation_id'),
+ )
+ }
+
+ const scroller = useScrollPosition()
+ const tryScrollTo = async (id) => {
+ if (!id) {
+ return
+ }
+ if (isPage.value) {
+ router.push({ name: 'conversation', params: { statusId: id } })
+ }
+ setFocused(id)
+ const target = document.querySelector(`.Status[data-status-id=${id}]`)
+ return await scroller.scrollIntoView(target, { block: 'center' })
+ }
+
const { statusId } = toRefs(props)
const router = useRouter()
- // # Main Configuration
+ // # Main Configuration / global state
const { mergedConfig } = storeToRefs(useMergedConfigStore())
const { mastoUserSocketStatus } = storeToRefs(useStreamingStore())
const displayStyle = computed(() => mergedConfig.value.conversationDisplay)
@@ -86,12 +107,20 @@ export default {
mergedConfig.value.useStreamingApi &&
mastoUserSocketStatus === WSConnectionStatus.JOINED,
)
-
- // # Misc
- const loadStatusError = ref(null)
const { layoutType } = storeToRefs(useInterfaceStore())
const mobileLayout = computed(() => layoutType.value === 'mobile')
+ // # Conversation Expansion
+ const expanded = ref(false)
+ const { isPage } = toRefs(props)
+ const isExpanded = computed(() => !!(expanded.value || isPage.value))
+ const toggleExpanded = () => {
+ expanded.value = !expanded.value
+ }
+ provide('isExpanded', isExpanded)
+ provide('isPage', isPage)
+ provide('expandable', true)
+
// # Focus
const focusedId = ref(statusId.value)
const focused = computed(() => (isExpanded.value ? focusedId.value : null))
@@ -109,52 +138,28 @@ export default {
provide('focused', focused)
// # Main things
- const getStatusObject = (id) => useStatusesStore().allStatuses.get(id)
- const getConversationId = (statusId) => {
- const status = getStatusObject(statusId)
- return get(
- status,
- 'retweeted_status.statusnet_conversation_id',
- get(status, 'statusnet_conversation_id'),
- )
- }
- const status = computed(() => getStatusObject(focusedId.value))
+ const {
+ currentStatus,
+ conversation,
+ replies,
+ getReplies,
+ fetchConversation,
+ loadError,
+ } = useConversation(focusedId, isExpanded)
- 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,
- })
+ watch(expanded, async (value) => {
+ if (value) {
+ await fetchConversation()
} else {
- try {
- loadStatusError.value = null
-
- const { data: status } = await apiFetchStatus({
- id: statusId.value,
- credentials: useOAuthStore().token,
- })
-
- useStatusesStore().addNewStatuses({ statuses: [status] })
- fetchConversation()
- } catch (error) {
- console.error(error)
- loadStatusError.value = error
- }
+ resetDisplayState()
}
- }
+ if (isPage.value) return
+ await tryScrollTo(currentStatus.value.id)
+ }, { flush: 'post' })
+
const resetDisplayState = () => {
setFocused(statusId.value)
- threadDisplay.value = new Map()
+ resetThreadDisplay()
}
watch(statusId, (newVal, oldVal) => {
const newConversationId = getConversationId(newVal)
@@ -171,243 +176,11 @@ export default {
}
})
- 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 conversation = computed(() => {
- if (!status.value) {
- return []
- }
-
- if (!isExpanded.value) {
- return [status.value]
- }
-
- const conversation = useStatusesStore().conversations.get(
- conversationId.value,
- )
-
- return [...conversation.keys()]
- .map((k) => useStatusesStore().allStatuses.get(k))
- .filter((status) => status.type != 'repeat') // Old backend behavior?
- .toSorted(sortById)
- })
- 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 statusReplies = computed(() => {
- return getReplies(status.value.id)
- })
-
- provide('conversation', conversation)
- provide('replies', replies)
-
- // # 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()
- }
- })
- provide('isExpanded', isExpanded)
- provide('isPage', isPage)
-
// Component created
if (isPage.value) {
fetchConversation()
}
- // # Virtual scrolling stuff
- const fontSizeSetting = computed(() => mergedConfig.value.textSize)
- const fontSize = computed(() => {
- // reading fontSizeSetting to make computed react to it
- fontSizeSetting.value
- const string = window
- .getComputedStyle(document.body)
- .getPropertyValue('font-size')
- return Number.parseInt(string.slice(0, -2), 10) // remove the 'px'
- })
- const mutedStatusHeight = computed(() => {
- return fontSize.value * 1.5
- })
- const normalStatusHeight = computed(() => {
- return fontSize.value * 10
- })
- const heights = ref(new Map())
- const totalHeight = computed(() =>
- conversation.value.reduce((acc, item) => {
- if (heights.value.has(item.id)) {
- return acc + heights.value.get(item.id)
- } else if (item.muted) {
- return acc + mutedStatusHeight.value
- } else {
- return acc + normalStatusHeight.value
- }
- }, 0),
- )
-
- const body = useTemplateRef('body')
- const updateVirtualHeight = ({ id, height }) => {
- heights.value.set(id, height)
- }
-
- const { y: topScrollBoundary } = useScrollPosition()
- const { height: windowHeight } = useWindowSize()
-
- const realTopScrollBoundary = ref(0)
- const realBottomScrollBoundary = ref(0)
- const updateBoundaries = () => {
- if (!body.value) return // Not mounted yet
-
- const { top } = body.value.getBoundingClientRect()
-
- const distanceItemTopToWindowTop = 0 - top
- const distanceItemTopToWindowBottom = windowHeight.value - top
-
- realTopScrollBoundary.value = distanceItemTopToWindowTop
- realBottomScrollBoundary.value = distanceItemTopToWindowBottom
- }
-
- watch(topScrollBoundary, updateBoundaries)
- watch(totalHeight, updateBoundaries)
- onMounted(updateBoundaries)
-
- const buffer = normalStatusHeight.value * 2
-
- const unsuspendibleIds = ref(new Set())
- const onStatusSuspendStateChange = ({ id, suspend }) => {
- if (!suspend) {
- unsuspendibleIds.value.add(id)
- } else {
- unsuspendibleIds.value.delete(id)
- }
- }
-
- const heightChartLinear = computed(() => {
- // Map every height and suspendable state
- const chart = conversation.value.map(({ id }) => {
- const status = getStatusObject(id)
- const height =
- (() => {
- if (heights.value.has(id)) {
- return heights.value.get(id)
- } else if (status?.muted) {
- return mutedStatusHeight.value
- } else {
- return normalStatusHeight.value
- }
- })() + 1 //including border
- const suspendable = !unsuspendibleIds.value.has(id)
- return { id, height, suspendable, status }
- })
-
- // Walk over the list to set top offsets
- chart.reduce((sum, item) => {
- item.top = sum
- return sum + item.height
- }, 0)
-
- // Determine visibility state
- chart.forEach((heightChartItem) => {
- const itemBottomBoundary = heightChartItem.top + heightChartItem.height
- const itemTopBoundary = heightChartItem.top
-
- const finalTopScrollBoundary = realTopScrollBoundary.value - buffer
- const finalBottomScrollBoundary =
- realBottomScrollBoundary.value + buffer
-
- // console.log(
- // 'TOP SCROLL',
- // itemBottomBoundary > finalTopScrollBoundary,
- // itemBottomBoundary, finalTopScrollBoundary,
- // )
- // console.log(
- // 'BOTTOM SCROLL',
- // itemTopBoundary < finalBottomScrollBoundary,
- // itemTopBoundary, finalBottomScrollBoundary,
- // )
-
- // To be visible, item's bottom boundary shoud be below top scroll boundary)
- const belowTopBoundary = itemBottomBoundary > finalTopScrollBoundary
- // To be visible, item's top boundary shoud be above bottom scroll boundary)
- const aboveBottomBoundary = itemTopBoundary < finalBottomScrollBoundary
- // This accounts for the case where item's boundaries exceed scroll boundary
-
- heightChartItem.visible = belowTopBoundary && aboveBottomBoundary
- })
-
- // Group invisible statuses into spacers
- return chart.reduce((acc, heightChartItem) => {
- const { suspendable, visible, height, top, bottom, id, status } =
- heightChartItem
- const present = visible || !suspendable
- if (present) {
- return [...acc, { type: 'status', height, top, bottom, id, status }]
- } else {
- const previousItem = acc[acc.length - 1]
- const spacer =
- previousItem?.type === 'spacer'
- ? previousItem
- : {
- type: 'spacer',
- top: Number.POSITIVE_INFINITY,
- bottom: Number.POSITIVE_INFINITY,
- height: 0,
- ids: new Set(),
- }
-
- spacer.ids.add(id)
- spacer.id = [...spacer.ids].join()
- spacer.height += height
- if (top < spacer.top) spacer.top = top
- if (bottom < spacer.bottom) spacer.bottom = bottom
-
- if (previousItem?.type === 'spacer') {
- return acc
- } else {
- return [...acc, spacer]
- }
- }
- }, [])
- })
-
// # Misc UI things
const firstStatus = computed(() => conversation.value[0])
const lastStatus = computed(
@@ -420,179 +193,100 @@ export default {
// # Linear style stuff
const isLinearView = computed(() => displayStyle.value !== 'tree')
+ const linearElement = useTemplateRef('linear')
+ const linearScrollCompensation = computed(() => isLinearView.value && isExpanded.value)
+ const {
+ heightChart: heightChartLinear,
+ changeSuspendState: changeSuspendStateLinear,
+ updateVirtualHeight: updateVirtualHeightLinear,
+ } = useVirtualScrolling(conversation, linearElement, scroller, linearScrollCompensation, currentStatus)
// # Tree style stuff
const isTreeView = computed(() => displayStyle.value === 'tree')
-
- // ## Tree state
- // ### 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 topLevel = computed(() =>
- [...ancestors.value.entries()]
- .filter(([id, ancestors]) => ancestors.size === 0)
- .map(([id]) => getStatusObject(id)),
- )
- 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)
-
- // ### 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 {
+ topLevel,
+ currentAncestors,
+ threadDisplay,
+ showThreadRecursively,
+ resetThreadDisplay,
+ } = useTreeConversationTopology(conversation, replies, focusedId)
provide('threadDisplay', threadDisplay)
- provide('threadDisplayDefault', threadDisplayDefault)
- 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')
- }
+ const ancestorsElement = useTemplateRef('ancestors')
+ const treeScrollCompensation = computed(() => isTreeView.value && isExpanded.value)
+ const {
+ heightChart: heightChartAncestors,
+ changeSuspendState: changeSuspendStateAncestors,
+ updateVirtualHeight: updateVirtualHeightAncestors,
+ } = useVirtualScrolling(currentAncestors, ancestorsElement, scroller, treeScrollCompensation)
+
+ const currentLevel = computed(() => [currentStatus.value].filter(Boolean))
+ const currentLevelElement = useTemplateRef('currentLevel')
+ const {
+ heightChart: heightChartCurrentLevel,
+ totalHeight: totalHeightCurrentLevel,
+ changeSuspendState: changeSuspendStateCurrentLevel,
+ updateVirtualHeight: updateVirtualHeightCurrentLevel,
+ } = useVirtualScrolling(currentLevel, currentLevelElement, scroller, false)
- // ## Derived values and config
const treeViewIsSimple = computed(
() => !mergedConfig.value.conversationTreeAdvanced,
)
- 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)
- })
const shouldShowAllConversationButton = computed(
() => currentAncestors.value.length > 0 && topLevel.value.length > 1,
)
const shouldShowAncestors = computed(
- () => isExpanded.value && ancestors.value.get(focusedId.value) != null,
+ () => isExpanded.value && heightChartAncestors.value.length > 0,
)
const shouldFadeAncestors = computed(
() => mergedConfig.value.conversationTreeFadeAncestors,
)
- const shouldShowOtherRepliesButton = computed(
- () => mergedConfig.value.conversationOtherRepliesButton === 'below',
- )
// # Scrolling
- const tryScrollTo = (id) => {
- if (!id) {
- return
- }
- if (isPage.value) {
- router.push({ name: 'conversation', params: { statusId: 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 `focused` 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 `focused` watcher.
- nextTick(() => {
- setFocused(id)
- })
- }
- const diveIntoStatus = (id) => {
- tryScrollTo(id)
- }
- const diveToTopLevel = () => {
- tryScrollTo(currentAncestors.value[0].id)
- }
+ const diveIntoStatus = (id) => tryScrollTo(id)
+ const diveToTopLevel = () => tryScrollTo(currentAncestors.value[0].id)
return {
// # Misc
- loadStatusError,
+ loadError,
mobileLayout,
- // # Focus
- focused,
- setFocused,
-
- // # Main things
- status,
- statusReplies,
- getReplies,
- conversation,
-
// # Conversation Expansion
isPage,
isExpanded,
toggleExpanded,
- // # Virtual scrolling stuff
- onStatusSuspendStateChange,
- updateVirtualHeight,
+ // # Focus
+ focused,
+ setFocused,
+
+ // # Main things
+ conversation,
+ currentStatus,
+ getReplies,
// # Misc UI things
getStatusClasses,
// # Linear style stuff
isLinearView,
+
+ // ## Linear virtual scrolling
heightChartLinear,
+ changeSuspendStateLinear,
+ updateVirtualHeightLinear,
// # Tree style stuff
isTreeView,
+ // ## Tree virtual scrolling
+ heightChartAncestors,
+ changeSuspendStateAncestors,
+ updateVirtualHeightAncestors,
+ heightChartCurrentLevel,
+ changeSuspendStateCurrentLevel,
+ updateVirtualHeightCurrentLevel,
+
// ## Tree state
// ### Topology
topLevel,
@@ -601,12 +295,11 @@ export default {
// ### Thread Display
showThreadRecursively,
- // ## Derived values and config
+ // ### Derived values and config
treeViewIsSimple,
shouldShowAllConversationButton,
shouldShowAncestors,
shouldFadeAncestors,
- shouldShowOtherRepliesButton,
// # Scrolling
diveToTopLevel,
diff --git a/src/components/conversation/conversation.scss b/src/components/conversation/conversation.scss
index ae06dbfa7..daf3cb11e 100644
--- a/src/components/conversation/conversation.scss
+++ b/src/components/conversation/conversation.scss
@@ -40,24 +40,6 @@
/* stylelint-enable declaration-no-important */
}
- .thread-ancestor-dive-box {
- padding-left: var(--status-margin);
- border-bottom: 1px solid var(--border);
- border-radius: 0;
-
- /* Make the button stretch along the whole row */
- &,
- &-inner {
- display: flex;
- align-items: stretch;
- flex-direction: column;
- }
- }
-
- .thread-ancestor-dive-box-inner {
- padding: var(--status-margin);
- }
-
.thread-ancestors + .thread-tree > .conversation-status {
border-top: 1px solid var(--border);
}
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 9b8d342c2..3a61decf5 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -37,12 +37,12 @@
/>
-
+
- {{ $t('status.load_error', { error: loadStatusError }) }}
+ {{ $t('status.load_error', { error: loadError }) }}
-
-
-
-
-
-
-
- {{ $t('status.ancestor_follow', { numReplies: statusReplies.size - 1 }) }}
-
-
-
-
-
+ v-if="element.type === 'spacer'"
+ class="virtual-spacer"
+ :style="{ height: element.height + 'px' }"
+ />
-
+
+
+
+
+ @goto="setFocused"
+ @dive="diveIntoStatus"
+ @toggle-expanded="toggleExpanded"
+ @show-thread-recursively="showThreadRecursively"
+ @suspendable-state-change="changeSuspendStateCurrentLevel"
+ @height-change="updateVirtualHeightCurrentLevel"
+ />
+
+
+
diff --git a/src/components/conversation/useScrollPosition.js b/src/components/conversation/useScrollPosition.js
deleted file mode 100644
index 7772a35a4..000000000
--- a/src/components/conversation/useScrollPosition.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { onMounted, onUnmounted, ref } from 'vue'
-
-export function useScrollPosition() {
- const x = ref(0)
- const y = ref(0)
-
- const update = (e) => {
- x.value = window.scrollX
- y.value = window.scrollY
- }
-
- onMounted(() => {
- window.addEventListener('scroll', update)
- update()
- })
- onUnmounted(() => {
- window.removeEventListener('scroll', update)
- })
-
- return { x, y }
-}
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 495503491..7b4649b6a 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -107,6 +107,10 @@ const Status = {
ignoreMute: Boolean,
threadDisplayState: String,
+ conversationRank: {
+ type: String,
+ default: 'linear',
+ },
},
emits: [
'goto',
@@ -116,9 +120,18 @@ const Status = {
'heightChange',
],
inject: {
- profileUserId: { default: null },
- isPage: { default: false },
- isExpanded: { default: false },
+ profileUserId: {
+ default: null,
+ },
+ isPage: {
+ default: false,
+ },
+ isExpanded: {
+ default: false,
+ },
+ expandable: {
+ default: false,
+ },
},
data() {
return {
@@ -134,6 +147,12 @@ const Status = {
useScrobblesStore().getLatestScrobble(this.status.user.id)
},
computed: {
+ rootClasses() {
+ return [
+ {'-focused': this.focused, '-conversation': !this.isPage && this.isExpanded },
+ `-conversation-rank-${this.conversationRank}`,
+ ]
+ },
// Whatever we're given to work with
status() {
return this.statusoid ?? useStatusesStore().allStatuses.get(this.statusId)
@@ -173,9 +192,12 @@ const Status = {
simpleTree() {
return !this.mergedConfig.conversationTreeAdvanced
},
- showOtherRepliesAsButton() {
+ showOtherRepliesInside() {
return this.mergedConfig.conversationOtherRepliesButton === 'inside'
},
+ showOtherRepliesBelow() {
+ return this.mergedConfig.conversationOtherRepliesButton === 'below'
+ },
showReasonMutedThread() {
return (
(this.mainStatus.thread_muted || this.repeatStatus?.thread_muted) &&
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index 74a50355a..d198f8d7e 100644
--- a/src/components/status/status.scss
+++ b/src/components/status/status.scss
@@ -3,6 +3,8 @@
white-space: normal;
overflow-wrap: break-word;
text-wrap: pretty;
+ display: flex;
+ flex-direction: column;
&:hover {
--_still-image-img-visibility: visible;
@@ -281,6 +283,10 @@
margin-top: var(--status-margin);
}
+ .status-action-buttons {
+ margin-top: var(--status-margin);
+ }
+
.muted {
padding: 0.25em 0.6em;
height: 1.2em;
@@ -372,7 +378,9 @@
}
}
- .status-action-buttons {
- margin-top: var(--status-margin);
+ .thread-tree-show-replies-button {
+ display: block;
+ padding: var(--status-margin);
+ padding-left: var(--status-margin);
}
}
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index d78c52eaa..c0a1b0238 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -3,7 +3,7 @@
v-if="!hideStatus"
ref="root"
class="Status"
- :class="[{ '-focused': focused }, { '-conversation': !isPage && isExpanded }]"
+ :class="rootClasses"
>