Merge branch 'virtual-scrolling-2.0' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2026-09-09 03:18:50 +03:00
commit 24ac79a41d
23 changed files with 760 additions and 891 deletions

View file

@ -67,8 +67,6 @@
"@vue/devtools-api": "8.2.0",
"@vue/test-utils": "2.5.0",
"autoprefixer": "10.5.4",
"chalk": "6.0.0",
"cross-spawn": "7.0.6",
"iso-639-1": "3.1.6",
"playwright": "1.61.0",
"postcss": "8.5.28",

View file

@ -83,8 +83,9 @@ export default () => {
},
{
name: 'conversation',
path: '/notice/:id',
path: '/notice/:statusId',
component: ConversationPage,
props: true,
meta: { dontScroll: true },
},
{

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,15 @@
backdrop-filter: var(--__panel-backdrop-filter);
}
.conversation-status:not(.-last) {
border-bottom: 1px solid var(--border);
}
.conversation-status:not(.-last)
.conversation-status:not(.-first) {
border-radius: 0;
}
.conversation-dive-to-top-level-box {
padding: var(--status-margin);
border-bottom: 1px solid var(--border);
@ -49,26 +58,12 @@
padding: var(--status-margin);
}
.conversation-status {
border-bottom: 1px solid var(--border);
border-radius: 0;
}
.thread-ancestor-has-other-replies .conversation-status,
&:last-child:not(.-expanded) .conversation-status,
&.-expanded .conversation-status:last-child,
.thread-ancestor:last-child .conversation-status,
.thread-ancestor:last-child .thread-ancestor-dive-box,
&.-expanded .thread-tree .conversation-status {
border-bottom: none;
}
.thread-ancestors + .thread-tree > .conversation-status {
border-top: 1px solid var(--border);
}
/* expanded conversation in timeline */
&.status-fadein.-expanded .thread-body {
&.-expanded .thread-body {
border-left: 4px solid var(--cRed);
border-radius: var(--roundness);
border-top-left-radius: 0;
@ -76,7 +71,7 @@
border-bottom: 1px solid var(--border);
}
&.-expanded.status-fadein {
&.-expanded:not(.-page) {
--___margin: calc(var(--status-margin) / 2);
background: var(--background);

View file

@ -1,8 +1,8 @@
<template>
<div
v-if="!hide"
ref="root"
class="Conversation"
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
:class="{ '-expanded' : isExpanded, '-page': isPage, 'panel' : isExpanded }"
>
<div
v-if="isExpanded"
@ -19,19 +19,19 @@
</template>
</h1>
<button
v-if="collapsable"
v-if="!isPage"
class="button-unstyled -link"
@click.prevent="toggleExpanded"
>
{{ $t('timeline.collapse') }}
</button>
<QuickFilterSettings
v-if="!collapsable && mobileLayout"
v-if="isPage && mobileLayout"
:conversation="true"
class="rightside-button"
/>
<QuickViewSettings
v-if="!collapsable"
v-if="isPage"
:conversation="true"
class="rightside-button"
/>
@ -81,7 +81,7 @@
</template>
<template #text>
<span>
{{ $t('status.show_all_conversation', { numStatus: otherTopLevelCount }, otherTopLevelCount) }}
{{ $t('status.show_all_conversation', { numStatus: topLevel.length - 1 }, topLevel.length - 1) }}
</span>
</template>
</i18n-t>
@ -91,36 +91,27 @@
class="thread-ancestors"
>
<article
v-for="status in ancestorsOf(diveRoot)"
:key="status.id"
v-for="status in currentAncestors"
class="thread-ancestor"
:class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}"
:class="{'thread-ancestor-has-other-replies': statusReplies.size > 1, '-faded': shouldFadeAncestors}"
>
<Status
ref="statusComponent"
class="conversation-status status-fadein panel-body"
class="conversation-status panel-body"
:class="getStatusClasses(status)"
:status-id="status.id"
:replies="getReplies(status.id)"
:replies="statusReplies"
: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"
:simple-tree="treeViewIsSimple"
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
:focused="focused === status.id"
can-dive
@goto="setFocused"
@dive="() => diveIntoStatus(status.id)"
@dive="diveIntoStatus(status.id)"
@suspendable-state-change="onStatusSuspendStateChange"
@height-change="updateVirtualHeight"
/>
<div
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
v-if="shouldShowOtherRepliesButton && statusReplies.size > 1"
class="thread-ancestor-dive-box"
>
<div
@ -140,7 +131,7 @@
</template>
<template #text>
<span>
{{ $t('status.ancestor_follow', { numReplies: getReplies(status.id, getReplies(status.id).length - 1).length - 1 }) }}
{{ $t('status.ancestor_follow', { numReplies: statusReplies.size - 1 }) }}
</span>
</template>
</i18n-t>
@ -149,32 +140,13 @@
</article>
</div>
<ThreadTree
v-for="status in showingTopLevel"
:key="status.id"
ref="statusComponent"
:depth="0"
:status-id="status.id"
:in-profile="inProfile"
:conversation="conversation"
:collapsable="collapsable"
:is-expanded="isExpanded"
:pinned-status-ids-object="pinnedStatusIdsObject"
:profile-user-id="profileUserId"
:get-replies="getReplies"
:focused="maybeFocused"
:toggle-expanded="toggleExpanded"
:simple="treeViewIsSimple"
:thread-display-status="threadDisplayStatus"
:show-thread-recursively="showThreadRecursively"
:total-reply-count="totalReplyCount"
:total-reply-depth="totalReplyDepth"
:can-dive="canDive"
:depth="0"
@goto="setFocused"
@dive="diveIntoStatus"
@toggle-expanded="toggleExpanded"
@show-thread-recursively="showThreadRecursively"
@suspendable-state-change="onStatusSuspendStateChange"
@height-change="updateVirtualHeight"
/>
@ -183,22 +155,24 @@
v-else-if="isLinearView"
class="thread-body"
>
<article>
<article
v-for="element in heightChartLinear"
class="panel-body"
:key="element.id ?? element.ids"
>
<div
v-if="element.type === 'spacer'"
class="virtual-spacer"
:style="{ height: element.height + 'px' }"
/>
<Status
v-for="status in conversation"
:key="status.id"
ref="statusComponent"
class="conversation-status status-fadein panel-body"
:status-id="status.id"
v-if="element.type === 'status'"
class="conversation-status"
:class="getStatusClasses(status)"
:status-id="element.status.id"
:replies="getReplies(status.id)"
:expandable="!isExpanded"
: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"
:focused="focused === element.id || focused === element.status.retweeted_status?.id"
@goto="setFocused"
@toggle-expanded="toggleExpanded"
@ -209,11 +183,6 @@
</div>
</div>
</div>
<div
v-else
class="Conversation -hidden"
:style="hiddenStyle"
/>
</template>
<script src="./conversation.js"></script>

View file

@ -0,0 +1,21 @@
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 }
}

View file

@ -0,0 +1,18 @@
import { onMounted, onUnmounted, ref } from 'vue'
export function useWindowSize() {
const height = ref(0)
const width = ref(0)
const update = () => {
height.value = window.innerHeight
width.value = window.innerWidth
}
onMounted(() => window.addEventListener('resize', update))
onUnmounted(() => window.removeEventListener('resize', update))
update()
return { height, width }
}

View file

@ -13,7 +13,7 @@
<template #statusLink>
<router-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') }}
</router-link>

View file

@ -166,7 +166,7 @@
>
<router-link
v-if="notification.status"
:to="{ name: 'conversation', params: { id: notification.status.id } }"
:to="{ name: 'conversation', params: { statusId: notification.status.id } }"
class="timeago-link faint"
>
<Timeago

View file

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

View file

@ -38,7 +38,7 @@
<router-link
v-for="status in report.statuses"
:key="status.id"
:to="{ name: 'conversation', params: { id: status.id } }"
:to="{ name: 'conversation', params: { statusId: status.id } }"
class="reported-status"
>
<div class="reported-status-heading">

View file

@ -96,25 +96,17 @@ const Status = {
props: {
statusId: String,
statusoid: Object,
replies: Array,
replies: Set,
expandable: Boolean,
focused: Boolean,
compact: Boolean,
isPreview: Boolean,
noHeading: Boolean,
inlineExpanded: Boolean,
inProfile: Boolean,
inConversation: Boolean,
inQuote: Boolean,
profileUserId: String,
simpleTree: Boolean,
showOtherRepliesAsButton: Boolean,
canDive: Boolean,
ignoreMute: Boolean,
threadDisplayStatus: String,
threadDisplayState: String,
},
emits: [
'goto',
@ -123,8 +115,14 @@ const Status = {
'suspendableStateChange',
'heightChange',
],
inject: {
profileUserId: { default: null },
isPage: { default: false },
isExpanded: { default: false },
},
data() {
return {
resizeObserver: new ResizeObserver(this.updateVirtualHeight),
replying: false,
unmuted: false,
mediaPlaying: new Set(),
@ -140,6 +138,12 @@ const Status = {
status() {
return this.statusoid ?? useStatusesStore().allStatuses.get(this.statusId)
},
inConversation() {
return this.isExpanded
},
inProfile() {
return this.profileUserId != null
},
// Status repeated
repeatedStatus() {
if (this.status.retweeted_status === undefined) return undefined
@ -166,6 +170,12 @@ const Status = {
user() {
return useUsersStore().findUser(this.mainStatus.user.id)
},
simpleTree() {
return !this.mergedConfig.conversationTreeAdvanced
},
showOtherRepliesAsButton() {
return this.mergedConfig.conversationOtherRepliesButton === 'inside'
},
showReasonMutedThread() {
return (
(this.mainStatus.thread_muted || this.repeatStatus?.thread_muted) &&
@ -436,10 +446,10 @@ const Status = {
return !this.replying && this.mediaPlaying.size === 0
},
inThreadForest() {
return !!this.threadDisplayStatus
return !!this.threadDisplayState
},
threadShowing() {
return this.threadDisplayStatus === 'showing'
return this.threadDisplayState === 'showing'
},
visibilityLocalized() {
return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility)
@ -553,47 +563,22 @@ const Status = {
// FIXME
this.controlledToggleThreadDisplay()
},
scrollIfFocused(focused) {
if (this.$el.getBoundingClientRect == null) return
if (focused) {
const rect = this.$el.getBoundingClientRect()
if (rect.top < 100) {
// Post is above screen, match its top to screen top
window.scrollBy(0, rect.top - 100)
} else if (rect.height >= window.innerHeight - 50) {
// Post we want to see is taller than screen so match its top to screen top
window.scrollBy(0, rect.top - 100)
} else if (rect.bottom > window.innerHeight - 50) {
// Post is below screen, match its bottom to screen bottom
window.scrollBy(0, rect.bottom - window.innerHeight + 50)
}
}
},
onTransitionEnd() {
this.$nextTick(() => {
this.$emit('heightChange')
updateVirtualHeight(e) {
const [entry] = e
this.$emit('heightChange', {
id: this.status.id,
height: entry.contentRect.height,
element: this.$el,
})
},
},
mounted() {
this.resizeObserver.observe(this.$el)
},
unmounted() {
this.resizeObserver.disconnect()
},
watch: {
status: {
deep: true,
handler() {
this.$emit('heightChange')
},
},
unmuted() {
this.$emit('heightChange')
},
error() {
this.$emit('heightChange')
},
replying() {
this.$emit('heightChange')
},
focused: function (id) {
this.scrollIfFocused(id)
},
'mainStatus.repeat_num': function (num) {
// refetch repeats when repeat_num is changed in any way
if (this.focused && this.repeatedBy.size !== num) {

View file

@ -271,21 +271,6 @@
}
}
.status-fadein {
animation-duration: 0.4s;
animation-name: fadein;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.status-actions {
position: relative;
width: 100%;

View file

@ -3,7 +3,7 @@
v-if="!hideStatus"
ref="root"
class="Status"
:class="[{ '-focused': focused }, { '-conversation': inlineExpanded }]"
:class="[{ '-focused': focused }, { '-conversation': !isPage && isExpanded }]"
>
<div
v-if="error"
@ -180,7 +180,7 @@
</span>
<router-link
class="timeago faint"
:to="{ name: 'conversation', params: { id: status.id } }"
:to="{ name: 'conversation', params: { statusId: status.id } }"
>
<Timeago
:time="mainStatus.created_at"
@ -199,7 +199,7 @@
/>
</span>
<button
v-if="expandable && !isPreview"
v-if="!isExpanded && !isPreview"
class="button-unstyled"
:title="$t('status.expand')"
@click.prevent="toggleExpanded"
@ -222,7 +222,7 @@
/>
</button>
<button
v-if="inThreadForest && replies?.length && !simpleTree"
v-if="inThreadForest && replies?.size && !simpleTree"
class="button-unstyled"
:title="threadShowing ? $t('status.thread_hide') : $t('status.thread_show')"
:aria-expanded="threadShowing ? 'true' : 'false'"
@ -235,7 +235,7 @@
/>
</button>
<button
v-if="canDive && !simpleTree"
v-if="isExpanded && !simpleTree"
class="button-unstyled"
:title="$t('status.show_only_conversation_under_this')"
@click.prevent="$emit('dive')"
@ -423,16 +423,16 @@
/>
<div
v-if="inConversation && !isPreview && replies?.length"
v-if="inConversation && !isPreview && replies?.size"
class="replies"
>
<button
v-if="showOtherRepliesAsButton && replies.length > 1"
v-if="showOtherRepliesAsButton && replies.size > 1"
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')"
>
{{ $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>
<span
v-else
@ -441,7 +441,7 @@
{{ $t('status.replies_list') }}
</span>
<StatusPopover
v-for="reply in replies"
v-for="reply in replies.values()"
:key="reply.id"
:status-id="reply.id"
>
@ -454,10 +454,7 @@
</StatusPopover>
</div>
<Transition
@after-leave="onTransitionEnd"
name="fade"
>
<Transition name="fade">
<div
v-if="shouldDisplayFavsAndRepeats"
class="favs-repeated-users"
@ -549,7 +546,6 @@
@posted="closeReplyForm"
@draft-done="closeReplyForm"
@close-accepted="closeReplyForm"
@resize="$emit('heightChange')"
/>
</div>
</template>

View file

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

View file

@ -20,7 +20,7 @@
:key="status.id"
:statusoid="status"
:is-preview="true"
class="conversation-status status-fadein panel-body"
class="conversation-status panel-body"
/>
</div>
</div>

View file

@ -1,3 +1,5 @@
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faAngleDoubleDown,
@ -10,33 +12,85 @@ const ThreadTree = {
components: {},
name: 'ThreadTree',
props: {
depth: Number,
statusId: String,
inProfile: Boolean,
conversation: Array,
collapsable: Boolean,
isExpanded: Boolean,
pinnedStatusIdsObject: Object,
profileUserId: String,
focused: String,
getReplies: Function,
toggleExpanded: Function,
simple: Boolean,
canDive: Boolean,
threadDisplayStatus: Object,
showThreadRecursively: Function,
totalReplyCount: Object,
totalReplyDepth: Object,
depth: Number,
},
emits: ['suspendableStateChange', 'goto', 'dive', 'heightChange'],
emits: [
'suspendableStateChange',
'goto',
'dive',
'toggleExpanded',
'showThreadRecursively',
],
inject: [
'conversation',
'focused',
'replies',
'threadDisplay',
'threadDisplayDefault',
'isExpanded',
'isPage',
],
computed: {
currentReplies() {
return this.getReplies(this.statusId).map(({ id }) => id)
return [...this.getReplies(this.statusId)].map(({ id }) => id)
},
simple() {
return !useMergedConfigStore().mergedConfig.conversationTreeAdvanced
},
threadShowing() {
return this.threadDisplayStatus[this.statusId] === 'showing'
const result =
this.threadDisplay.get(this.statusId) ??
this.threadDisplayDefault.get(this.statusId)
return result === 'showing'
},
canDive() {
return this.isExpanded
},
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()
},
},
}

View file

@ -2,27 +2,17 @@
<article class="thread-tree">
<Status
:key="statusId"
ref="statusComponent"
class="conversation-status conversation-status-treeview panel-body"
:status-id="statusId"
:replies="getReplies(statusId)"
:inline-expanded="collapsable && isExpanded"
:expandable="!isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
:in-conversation="isExpanded"
:focused="focused === statusId"
:in-profile="inProfile"
:profile-user-id="profileUserId"
class="conversation-status conversation-status-treeview status-fadein panel-body"
:simple-tree="simple"
:thread-display-status="threadDisplayStatus[statusId]"
:can-dive="canDive"
:thread-display-state="threadDisplay.get(statusId)"
@dive="$emit('dive', statusId)"
@goto="$emit('goto', statusId)"
@toggle-expanded="toggleExpanded"
@suspendable-state-change="e => $emit('suspendableStateChange', e)"
@height-change="e => $emit('heightChange', e)"
@toggle-expanded="$emit('toggleExpanded', statusId)"
@suspendable-state-change="$emit('suspendableStateChange', e)"
/>
<div
v-if="currentReplies.length > 0 && threadShowing"
@ -31,32 +21,14 @@
<ThreadTree
v-for="replyStatusId in currentReplies"
:key="replyStatusId"
ref="childComponent"
:depth="depth + 1"
:status-id="replyStatusId"
:in-profile="inProfile"
:conversation="conversation"
:collapsable="collapsable"
:is-expanded="isExpanded"
:pinned-status-ids-object="pinnedStatusIdsObject"
:profile-user-id="profileUserId"
:get-replies="getReplies"
:focused="focused"
:toggle-expanded="toggleExpanded"
:simple="simple"
:thread-display-status="threadDisplayStatus"
:show-thread-recursively="showThreadRecursively"
:total-reply-count="totalReplyCount"
:total-reply-depth="totalReplyDepth"
:can-dive="canDive"
@show-thread-recursively="(e) => $emit('showThreadRecursively', e)"
@goto="(e) => $emit('goto', e)"
@dive="(e) => $emit('dive', e)"
@suspendable-state-change="e => $emit('suspendableStateChange', e)"
@height-change="e => $emit('heightChange', e)"
@toggle-expanded="(e) => $emit('toggleExpanded', e)"
/>
</div>
<div
@ -88,7 +60,7 @@
tag="button"
keypath="status.thread_show_full_with_icon"
class="button-unstyled -link thread-tree-show-replies-button"
@click.prevent="showThreadRecursively(statusId)"
@click.prevent="$emit('showThreadRecursively', statusId)"
>
<template #icon>
<FAIcon

View file

@ -38,10 +38,14 @@ const Timeline = {
showScrollTop: false,
paused: false,
unfocused: false,
virtualScrollIndex: 0,
blockingClicks: false,
}
},
provide() {
return {
profileUserId: this.inProfile && this.timelineRef.argument,
}
},
components: {
ScrollTopButton,
Conversation,
@ -102,20 +106,7 @@ const Timeline = {
}
},
statusesToDisplay() {
if (!this.virtualScrollingEnabled) {
return new Set(this.filteredVisibleStatuses.map(({ id }) => id))
}
const amount = this.timeline.visibleStatusIds.size
const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80))
const min = Math.max(0, this.virtualScrollIndex - statusesPerSide)
const max = Math.min(amount, this.virtualScrollIndex + statusesPerSide)
return new Set(
this.filteredVisibleStatuses.slice(min, max).map(({ id }) => id),
)
},
virtualScrollingEnabled() {
return useMergedConfigStore().mergedConfig.virtualScrolling
return new Set(this.filteredVisibleStatuses.map(({ id }) => id))
},
...mapState(useInterfaceStore, {
mobileLayout: (store) => store.layoutType === 'mobile',
@ -135,7 +126,6 @@ const Timeline = {
}
window.addEventListener('keydown', this.handleShortKey)
window.addEventListener('scroll', this.handleScroll)
setTimeout(this.determineVisibleStatuses, 250)
},
unmounted() {
this.timelineChange(null, this.timelineRef)
@ -193,54 +183,6 @@ const Timeline = {
1000,
this,
),
determineVisibleStatuses() {
if (!this.$refs.timeline) return
if (!this.virtualScrollingEnabled) return
const statuses = this.$refs.timeline.children
if (statuses.length === 0) return
const cappedScrollIndex = Math.max(
0,
Math.min(this.virtualScrollIndex, statuses.length - 1),
)
const height = Math.max(document.body.offsetHeight, window.pageYOffset)
const centerOfScreen = window.pageYOffset + window.innerHeight * 0.5
// Start from approximating the index of some visible status by using the
// the center of the screen on the timeline.
let approxIndex = Math.min(
Math.floor(statuses.length * (centerOfScreen / height)),
statuses.length - 1,
)
let err = statuses[approxIndex].getBoundingClientRect().y
// if we have a previous scroll index that can be used, test if it's
// closer than the previous approximation, use it if so
const virtualScrollIndexY =
statuses[cappedScrollIndex].getBoundingClientRect().y
if (Math.abs(err) > virtualScrollIndexY) {
approxIndex = cappedScrollIndex
err = virtualScrollIndexY
}
// if the status is too far from viewport, check the next/previous ones if
// they happen to be better
while (err < -20 && approxIndex < statuses.length - 1) {
err += statuses[approxIndex].offsetHeight
approxIndex++
}
while (err > window.innerHeight + 100 && approxIndex > 0) {
approxIndex--
err -= statuses[approxIndex].offsetHeight
}
// this status is now the center point for virtual scrolling and visible
// statuses will be nearby statuses before and after it
this.virtualScrollIndex = approxIndex
},
scrollLoad() {
// TODO simplify this logic
const bodyBRect = document.body.getBoundingClientRect()
@ -253,7 +195,6 @@ const Timeline = {
}
},
handleScroll: throttle(function (e) {
this.determineVisibleStatuses()
this.scrollLoad(e)
}, 200),
handleVisibilityChange() {
@ -264,9 +205,6 @@ const Timeline = {
timelineRef(newTimeline, oldTimeline) {
this.timelineChange(newTimeline, oldTimeline)
},
filteredVisibleStatuses() {
this.determineVisibleStatuses()
},
newStatusCount(count) {
if (!useMergedConfigStore().mergedConfig.streaming) {
return

View file

@ -4,6 +4,10 @@
backdrop-filter: none;
}
.vue-recycle-scroller.timeline {
overflow: visible;
}
.timeline-placeholder {
background: var(--background);
display: flex;

View file

@ -78,20 +78,15 @@
</div>
<div :class="classes.body">
<div
ref="timeline"
class="timeline"
ref="timeline"
role="feed"
>
<Conversation
v-for="status in filteredVisibleStatuses"
:key="status.id"
role="listitem"
class="status-fadein"
:status-id="status.id"
:in-profile="inProfile"
:profile-user-id="timelineRef.argument"
:virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.has(status.id)"
collapsable
role="listitem"
/>
</div>
<template v-if="!hideEmpty && count === 0">

View file

@ -457,10 +457,6 @@ export const INSTANCE_DEFAULT_CONFIG_DEFINITIONS = {
description: 'Hide user stats (followers etc)',
default: false,
},
virtualScrolling: {
description: 'Timeline virtual scrolling',
default: true,
},
sensitiveByDefault: {
description: 'Assume attachments are NSFW by default',
default: false,

View file

@ -1499,11 +1499,6 @@ chai@^6.2.2:
resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e"
integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==
chalk@6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-6.0.0.tgz#a3bae843bc8454f41ef66ef3fd584dcf34dd805d"
integrity sha512-2uNTXIuTTxk7ciZgAU1BQcgnchcG0xXnrs6jzkQfj9SsRa9M2s5zE8WT96hS6KmG4MzWHSrvH43DF1m4XRkrFg==
chokidar@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-5.0.0.tgz#949c126a9238a80792be9a0265934f098af369a5"
@ -1608,15 +1603,6 @@ cropperjs@2.2.0:
"@cropper/elements" "^2.2.0"
"@cropper/utils" "^2.2.0"
cross-spawn@7.0.6:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
which "^2.0.1"
crossws@^0.4.12:
version "0.4.12"
resolved "https://registry.yarnpkg.com/crossws/-/crossws-0.4.12.tgz#3be6300b1e5a06db08e890e72eb1c7f6c809c9af"
@ -2617,11 +2603,6 @@ path-exists@^4.0.0:
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-scurry@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.2.tgz#6be0d0ee02a10d9e0de7a98bae65e182c9061f85"
@ -3083,18 +3064,6 @@ setprototypeof@~1.2.0:
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
dependencies:
shebang-regex "^3.0.0"
shebang-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
siginfo@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30"
@ -3603,13 +3572,6 @@ which@^1.3.1:
dependencies:
isexe "^2.0.0"
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
dependencies:
isexe "^2.0.0"
why-is-node-running@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04"