timeline fetcher improvements

This commit is contained in:
Henry Jameson 2026-08-19 02:03:23 +03:00
commit d7f19d75f1
8 changed files with 135 additions and 157 deletions

View file

@ -322,15 +322,15 @@ const Status = {
shouldNotMute() {
if (this.ignoreMute) return true
if (this.focused) return true
const { reblog } = this.status
const { reblog } = this.mainStatus
return (
((this.inProfile &&
// Don't mute user's posts on user timeline (except reblogs)
((!reblog && status.user.id === this.profileUserId) ||
((!reblog && this.mainStatus.user.id === this.profileUserId) ||
// Same as above but also allow self-reblogs
reblog?.user.id === this.profileUserId)) ||
// Don't mute statuses in muted conversation when said conversation is opened
(this.inConversation && status.thread_muted)) &&
(this.inConversation && this.mainStatus.thread_muted)) &&
// No excuses if post has muted words
!this.muteFilterHits.length > 0
)

View file

@ -38,7 +38,6 @@ const Timeline = {
showScrollTop: false,
paused: false,
unfocused: false,
bottomedOut: false,
virtualScrollIndex: 0,
blockingClicks: false,
}
@ -183,16 +182,7 @@ const Timeline = {
},
fetchOlderStatuses: throttle(
function () {
this.timeline.fetcher
.fetchAndUpdate({
older: true,
showImmediately: true,
})
.then(({ statuses }) => {
if (statuses?.length === 0) {
this.bottomedOut = true
}
})
this.timeline.fetcher.fetchOlder()
},
1000,
this,
@ -222,7 +212,7 @@ const Timeline = {
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
@ -250,7 +240,7 @@ const Timeline = {
const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -bodyBRect.y)
if (
this.timeline.loading === false &&
!this.timeline.fetcher.loading.value &&
this.$el.offsetHeight > 0 &&
window.innerHeight + window.pageYOffset >= height - 750
) {

View file

@ -90,19 +90,19 @@
:disabled="!embedded || !footerSlipgate"
>
<div
v-if="count===0"
v-if="count === 0"
class="new-status-notification text-center faint"
>
{{ $t('timeline.no_statuses') }}
</div>
<div
v-else-if="bottomedOut"
v-else-if="timeline.fetcher.bottomedOut"
class="new-status-notification text-center faint"
>
{{ $t('timeline.no_more_statuses') }}
</div>
<button
v-else-if="!timeline.loading"
v-else-if="!timeline.fetcher.loading"
class="button-unstyled -link"
@click.prevent="fetchOlderStatuses()"
>

View file

@ -1,132 +0,0 @@
import { promiseInterval } from '../promise_interval/promise_interval.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { ARGUMENT_MAP, useTimelinesStore } from 'src/stores/timelines.js'
import { useUsersStore } from 'src/stores/users.js'
import { fetchTimeline } from 'src/api/timelines.js'
const REPLY_VISIBILITY_TIMELINES = new Set([
'friends',
'public',
'publicAndExternal',
'bubble',
])
const fetchAndUpdate = (
{ timeline, argument, credentials },
{ older = false, showImmediately = false },
) => {
timeline.loading = true
const { hideMutedPosts, replyVisibility } =
useMergedConfigStore().mergedConfig
const loggedIn = useUsersStore().loggedIn
const args = { timeline: timeline.name, credentials }
const mainArg = ARGUMENT_MAP[timeline.name]
if (mainArg) args[mainArg] = argument
if (older) {
// When minId = 0 we need to fetch without maxId param
args.maxId = timeline.minId || null
} else {
args.sinceId = timeline.maxId || null
}
args.withMuted = !hideMutedPosts
if (loggedIn && REPLY_VISIBILITY_TIMELINES.has(timeline)) {
args.replyVisibility = replyVisibility
}
const numStatusesBeforeFetch = timeline.statusIds.size
return fetchTimeline(args)
.then((response) => {
const { data: statuses, pagination, timestamp } = response
if (
!older &&
statuses.length >= 20 &&
!timeline.loading &&
numStatusesBeforeFetch > 0
) {
useTimelinesStore().queueFlush(timeline.name, timeline.maxId)
}
const processed = useStatusesStore()
.addNewStatuses({ statuses, timestamp })
.filter(Boolean)
.map(({ id }) => id)
useTimelinesStore().addStatusesToTimeline(timeline.name, argument, {
statuses: processed,
showImmediately,
older,
pagination,
})
return { statuses, pagination }
})
.catch((error) => {
if (error.statusCode === 403 && timeline === 'favorites') {
useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable = false
return
}
console.error('Timeline Error', error)
useInterfaceStore().pushGlobalNotice({
level: 'error',
messageKey: 'timeline.error',
messageArgs: [error.message],
timeout: 5000,
})
})
.finally(() => {
timeline.loading = false
})
}
const timelineFetcher = (timeline, argument, credentials) => {
const state = {
interval: null,
}
const boundFetchAndUpdate = ({
showImmediately,
older,
} = {}) =>
fetchAndUpdate(
{
timeline,
argument,
credentials,
},
{
older,
showImmediately,
},
)
const startFetching = () => {
if (state.interval) throw new Error('Interval already exists!')
boundFetchAndUpdate({
showImmediately: timeline.visibleStatusIds.size === 0,
})
state.interval = promiseInterval(boundFetchAndUpdate, 10000)
}
const stopFetching = () => {
state.interval.stop()
state.interval = null
}
return {
startFetching,
stopFetching,
fetchAndUpdate: boundFetchAndUpdate,
}
}
export default timelineFetcher

View file

@ -0,0 +1,121 @@
import { ref } from 'vue'
import { promiseInterval } from 'src/services/promise_interval/promise_interval.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { ARGUMENT_MAP, useTimelinesStore } from 'src/stores/timelines.js'
import { useUsersStore } from 'src/stores/users.js'
import { fetchTimeline } from 'src/api/timelines.js'
const REPLY_VISIBILITY_TIMELINES = new Set([
'friends',
'public',
'publicAndExternal',
'bubble',
])
const timelineFetcher = (timeline, argument, credentials) => {
const loading = ref(false)
const bottomedOut = ref(false)
const interval = ref(null)
const fetchAndUpdate = ({ older = false, showImmediately = false } = {}) => {
loading.value = true
const { hideMutedPosts, replyVisibility } =
useMergedConfigStore().mergedConfig
const loggedIn = useUsersStore().loggedIn
const args = { timeline: timeline.name, credentials }
const mainArg = ARGUMENT_MAP[timeline.name]
if (mainArg) args[mainArg] = argument
if (older) {
// When minId = 0 we need to fetch without maxId param
args.maxId = timeline.minId || null
} else {
args.sinceId = timeline.maxId || null
}
args.withMuted = !hideMutedPosts
if (loggedIn && REPLY_VISIBILITY_TIMELINES.has(timeline)) {
args.replyVisibility = replyVisibility
}
const numStatusesBeforeFetch = timeline.statusIds.size
return fetchTimeline(args)
.then(({ data: statuses, pagination, timestamp }) => {
if (
!older &&
!loading.value &&
statuses.length >= 20 &&
numStatusesBeforeFetch > 0
) {
useTimelinesStore().queueFlush(timeline.name, timeline.maxId)
}
if (older && statuses.length === 0) {
bottomedOut.value = true
}
const processed = useStatusesStore()
.addNewStatuses({ statuses, timestamp })
.map(({ id }) => id)
useTimelinesStore().addStatusesToTimeline(timeline.name, argument, {
statuses: processed,
showImmediately,
older,
pagination,
})
return { statuses, pagination }
})
.catch((error) => {
if (error.statusCode === 403 && timeline === 'favorites') {
useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable = false
return
}
console.error('Timeline Error', error)
useInterfaceStore().pushGlobalNotice({
level: 'error',
messageKey: 'timeline.error',
messageArgs: [error.message],
timeout: 5000,
})
})
.finally(() => {
loading.value = false
})
}
const startFetching = () => {
if (interval.value) throw new Error('Interval already exists!')
fetchAndUpdate({
showImmediately: timeline.visibleStatusIds.size === 0,
})
interval.value = promiseInterval(fetchAndUpdate, 10000)
}
const stopFetching = () => {
interval.value.stop()
interval.value = null
}
return {
startFetching,
stopFetching,
fetchOlder: () => fetchAndUpdate({ showImmediately: true, older: true }),
fetchNewer: () => fetchAndUpdate({ showImmediately: true, older: false }),
loading,
bottomedOut,
}
}
export default timelineFetcher

View file

@ -7,7 +7,7 @@ import { useStatusesStore } from 'src/stores/statuses.js'
import { TIMELINE_STREAM_MAP, useStreamingStore } from 'src/stores/streaming.js'
import { useUsersStore } from 'src/stores/users.js'
import timelineFetcher from 'src/services/timeline_fetcher/timeline_fetcher.service.js'
import timelineFetcher from 'src/stores/fetchers/timeline_fetcher.js'
const emptyTl = (name, argument = null) => {
const result = {
@ -18,7 +18,6 @@ const emptyTl = (name, argument = null) => {
newStatusCount: 0,
maxId: '',
minId: '',
loading: false,
streaming: false,
flushMarker: 0,
fetcher: null,

View file

@ -559,8 +559,8 @@ export const useUsersStore = defineStore('users', {
return blockUser({ id, expiresIn }).then((result) => {
this.updateUserRelationships(result)
useStatusesStore().wipeUserStatuses(id)
useTimelinesStore().wipeUserStatuses(id)
const ids = useStatusesStore().wipeUserStatuses(id)
useTimelinesStore().wipeStatuses(ids)
})
},
blockUsers(data = []) {

View file

@ -1119,7 +1119,7 @@ describe('Users store', () => {
},
)
vi.spyOn(useTimelinesStore(), 'wipeUserStatuses').mockImplementation(
vi.spyOn(useTimelinesStore(), 'wipeStatuses').mockImplementation(
async () => {
/* no-op */
},
@ -1150,7 +1150,7 @@ describe('Users store', () => {
},
)
vi.spyOn(useTimelinesStore(), 'wipeUserStatuses').mockImplementation(
vi.spyOn(useTimelinesStore(), 'wipeStatuses').mockImplementation(
async () => {
/* no-op */
},