TIMELINES STORE YEAH BABY

This commit is contained in:
Henry Jameson 2026-08-11 18:54:54 +03:00
commit cb25b392cf
21 changed files with 426 additions and 734 deletions

View file

@ -1,10 +1,7 @@
import AuthForm from 'src/components/auth_form/auth_form.js' import AuthForm from 'src/components/auth_form/auth_form.js'
import BookmarkTimeline from 'src/components/bookmark_timeline/bookmark_timeline.vue'
import ConversationPage from 'src/components/conversation-page/conversation-page.vue' import ConversationPage from 'src/components/conversation-page/conversation-page.vue'
import NavPanel from 'src/components/nav_panel/nav_panel.vue' import NavPanel from 'src/components/nav_panel/nav_panel.vue'
import QuotesTimeline from 'src/components/quotes_timeline/quotes_timeline.vue'
import RemoteUserResolver from 'src/components/remote_user_resolver/remote_user_resolver.vue' import RemoteUserResolver from 'src/components/remote_user_resolver/remote_user_resolver.vue'
import TagTimeline from 'src/components/tag_timeline/tag_timeline.vue'
import Timeline from 'src/components/timeline/timeline.vue' import Timeline from 'src/components/timeline/timeline.vue'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
@ -40,7 +37,7 @@ export default (store) => {
path: '/main/all', path: '/main/all',
component: Timeline, component: Timeline,
props: () => ({ props: () => ({
timelineName: 'publicAndExternal', timelineRef: { name: 'publicAndExternal' },
}), }),
}, },
{ {
@ -48,7 +45,7 @@ export default (store) => {
path: '/main/public', path: '/main/public',
component: Timeline, component: Timeline,
props: () => ({ props: () => ({
timelineName: 'public', timelineRef: { name: 'public' },
}), }),
}, },
{ {
@ -57,17 +54,31 @@ export default (store) => {
component: Timeline, component: Timeline,
beforeEnter: validateAuthenticatedRoute, beforeEnter: validateAuthenticatedRoute,
props: () => ({ props: () => ({
timelineName: 'friends', timelineRef: { name: 'friends' },
}),
},
{
name: 'tag-timeline',
path: '/tag/:id',
component: Timeline,
props: (route) => ({
timelineRef: { name: 'tag', argument: route.params.id },
}),
},
{
name: 'bookmarks',
path: '/bookmarks',
component: Timeline,
props: (route) => ({
timelineRef: { name: 'bookmarks', argument: null },
}), }),
}, },
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
{ {
name: 'bubble', name: 'bubble',
path: '/bubble', path: '/bubble',
component: Timeline, component: Timeline,
props: () => ({ props: () => ({
timelineName: 'bubble', timelineRef: { name: 'bubble' },
}), }),
}, },
{ {
@ -84,7 +95,14 @@ export default (store) => {
meta: { dontScroll: true }, meta: { dontScroll: true },
beforeEnter: validateAuthenticatedRoute, beforeEnter: validateAuthenticatedRoute,
}, },
{ name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline }, {
name: 'quotes',
path: '/notice/:id/quotes',
component: Timeline,
props: (route) => ({
timelineRef: { name: 'quotes', argument: route.params.id },
}),
},
{ {
name: 'remote-user-profile-acct', name: 'remote-user-profile-acct',
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)', path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
@ -120,7 +138,7 @@ export default (store) => {
component: Timeline, component: Timeline,
beforeEnter: validateAuthenticatedRoute, beforeEnter: validateAuthenticatedRoute,
props: () => ({ props: () => ({
timelineName: 'dms', timelineRef: { name: 'dms' },
}), }),
}, },
{ {
@ -218,8 +236,10 @@ export default (store) => {
{ {
name: 'lists-timeline', name: 'lists-timeline',
path: '/lists/:id', path: '/lists/:id',
component: () => component: Timeline,
import('src/components/lists_timeline/lists_timeline.vue'), props: (route) => ({
timelineRef: { name: 'lists', argument: route.params.id },
}),
}, },
{ {
name: 'lists-edit', name: 'lists-edit',
@ -253,7 +273,10 @@ export default (store) => {
{ {
name: 'bookmark-folder', name: 'bookmark-folder',
path: '/bookmarks/:id', path: '/bookmarks/:id',
component: BookmarkTimeline, component: Timeline,
props: (route) => ({
timelineRef: { name: 'bookmarks', argument: route.params.id },
}),
}, },
{ {
name: 'bookmark-folder-edit', name: 'bookmark-folder-edit',

View file

@ -1,40 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
import { useStatusesStore } from 'src/stores/statuses.js'
const Bookmarks = {
created() {
useStatusesStore().clearTimeline({ timeline: 'bookmarks' })
this.$store.dispatch('startFetchingTimeline', {
timeline: 'bookmarks',
bookmarkFolderId: this.folderId || null,
})
},
components: {
Timeline,
},
computed: {
folderId() {
return this.$route.params.id
},
timeline() {
return this.$store.state.statuses.timelines.bookmarks
},
},
watch: {
folderId() {
useStatusesStore().clearTimeline({ timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
this.$store.dispatch('startFetchingTimeline', {
timeline: 'bookmarks',
bookmarkFolderId: this.folderId || null,
})
},
},
unmounted() {
useStatusesStore().clearTimeline({ timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
},
}
export default Bookmarks

View file

@ -1,10 +0,0 @@
<template>
<Timeline
:title="$t('nav.bookmarks')"
:timeline="timeline"
:timeline-name="'bookmarks'"
:bookmark-folder-id="folderId"
/>
</template>
<script src="./bookmark_timeline.js"></script>

View file

@ -1,20 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
const BubbleTimeline = {
components: {
Timeline,
},
computed: {
timeline() {
return this.$store.state.statuses.timelines.bubble
},
},
created() {
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
},
unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'bubble')
},
}
export default BubbleTimeline

View file

@ -1,48 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
import { useListsStore } from 'src/stores/lists.js'
import { useStatusesStore } from 'src/stores/statuses.js'
const ListsTimeline = {
data() {
return {
listId: null,
}
},
components: {
Timeline,
},
computed: {
timeline() {
return this.$store.state.statuses.timelines.list
},
},
watch: {
$route: function (route) {
if (route.name === 'lists-timeline' && route.params.id !== this.listId) {
this.listId = route.params.id
this.$store.dispatch('stopFetchingTimeline', 'list')
useStatusesStore().clearTimeline({ timeline: 'list' })
useListsStore().fetchList({ listId: this.listId })
this.$store.dispatch('startFetchingTimeline', {
timeline: 'list',
listId: this.listId,
})
}
},
},
created() {
this.listId = this.$route.params.id
useListsStore().fetchList({ listId: this.listId })
this.$store.dispatch('startFetchingTimeline', {
timeline: 'list',
listId: this.listId,
})
},
unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'list')
useStatusesStore().clearTimeline({ timeline: 'list' })
},
}
export default ListsTimeline

View file

@ -1,10 +0,0 @@
<template>
<Timeline
title="list.name"
:timeline="timeline"
:list-id="listId"
timeline-name="list"
/>
</template>
<script src="./lists_timeline.js"></script>

View file

@ -1,14 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
const Mentions = {
computed: {
timeline() {
return this.$store.state.statuses.timelines.mentions
},
},
components: {
Timeline,
},
}
export default Mentions

View file

@ -1,9 +0,0 @@
<template>
<Timeline
:title="$t('nav.interactions')"
:timeline="timeline"
:timeline-name="'mentions'"
/>
</template>
<script src="./mentions.js"></script>

View file

@ -1,38 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
import { useStatusesStore } from 'src/stores/statuses.js'
const QuotesTimeline = {
created() {
useStatusesStore().clearTimeline({ timeline: 'tag' })
this.$store.dispatch('startFetchingTimeline', {
timeline: 'quotes',
statusId: this.statusId,
})
},
components: {
Timeline,
},
computed: {
statusId() {
return this.$route.params.id
},
timeline() {
return this.$store.state.statuses.timelines.quotes
},
},
watch: {
statusId() {
useStatusesStore().clearTimeline({ timeline: 'tag' })
this.$store.dispatch('startFetchingTimeline', {
timeline: 'quotes',
statusId: this.statusId,
})
},
},
unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'quotes')
},
}
export default QuotesTimeline

View file

@ -1,10 +0,0 @@
<template>
<Timeline
:title="$t('nav.quotes')"
:timeline="timeline"
:timeline-name="'quotes'"
:status-id="statusId"
/>
</template>
<script src='./quotes_timeline.js'></script>

View file

@ -1,38 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
import { useStatusesStore } from 'src/stores/statuses.js'
const TagTimeline = {
created() {
useStatusesStore().clearTimeline({ timeline: 'tag' })
this.$store.dispatch('startFetchingTimeline', {
timeline: 'tag',
tag: this.tag,
})
},
components: {
Timeline,
},
computed: {
tag() {
return this.$route.params.tag
},
timeline() {
return this.$store.state.statuses.timelines.tag
},
},
watch: {
tag() {
useStatusesStore().clearTimeline({ timeline: 'tag' })
this.$store.dispatch('startFetchingTimeline', {
timeline: 'tag',
tag: this.tag,
})
},
},
unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'tag')
},
}
export default TagTimeline

View file

@ -10,10 +10,9 @@ import TimelineMenu from 'src/components/timeline_menu/timeline_menu.vue'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js' import { useStatusesStore } from 'src/stores/statuses.js'
import { useTimelinesStore } from 'src/stores/timelines.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
import timelineFetcher from 'src/services/timeline_fetcher/timeline_fetcher.service.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faArrowUp, faArrowUp,
@ -28,12 +27,8 @@ library.add(faCircleNotch, faCog, faMinus, faArrowUp, faCirclePlus, faCheck)
const Timeline = { const Timeline = {
props: { props: {
timelineName: String, timelineRef: Object,
userId: String, argument: String,
listId: String,
statusId: String,
bookmarkFolderId: String,
tag: String,
embedded: Boolean, embedded: Boolean,
count: Number, count: Number,
pinnedStatusIds: Set, pinnedStatusIds: Set,
@ -59,15 +54,16 @@ const Timeline = {
}, },
computed: { computed: {
timeline() { timeline() {
return useStatusesStore().timelines[this.timelineName] return useTimelinesStore()[this.timelineRef.name]
}, },
filteredVisibleStatuses() { filteredVisibleStatuses() {
return [...this.timeline.visibleStatuses.values()].filter( return [...this.timeline.visibleStatusesIds.keys()]
(status) => .filter(
this.timelineName !== 'user' || (id) =>
(status.id >= this.timeline.minId && this.timelineRef.name !== 'user' ||
status.id <= this.timeline.maxId), (id >= this.timeline.minId && id <= this.timeline.maxId),
) )
.map((id) => this.timeline.statuses.get(id))
}, },
filteredPinnedStatusIds() { filteredPinnedStatusIds() {
return (this.pinnedStatusIds || []).filter( return (this.pinnedStatusIds || []).filter(
@ -118,13 +114,15 @@ const Timeline = {
return keyBy(this.pinnedStatusIds) return keyBy(this.pinnedStatusIds)
}, },
statusesToDisplay() { statusesToDisplay() {
const amount = this.timeline.visibleStatuses.size const amount = this.timeline.visibleStatusesIds.size
const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80)) const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80))
const nonPinnedIndex = const nonPinnedIndex =
this.virtualScrollIndex - this.filteredPinnedStatusIds.length this.virtualScrollIndex - this.filteredPinnedStatusIds.length
const min = Math.max(0, nonPinnedIndex - statusesPerSide) const min = Math.max(0, nonPinnedIndex - statusesPerSide)
const max = Math.min(amount, nonPinnedIndex + statusesPerSide) const max = Math.min(amount, nonPinnedIndex + statusesPerSide)
return new Set([...this.timeline.visibleStatuses.keys()].slice(min, max)) return new Set(
[...this.timeline.visibleStatusesIds.keys()].slice(min, max),
)
}, },
virtualScrollingEnabled() { virtualScrollingEnabled() {
return useMergedConfigStore().mergedConfig.virtualScrolling return useMergedConfigStore().mergedConfig.virtualScrolling
@ -134,26 +132,7 @@ const Timeline = {
}), }),
}, },
created() { created() {
const store = this.$store this.timelineChange(this.timelineRef)
const credentials = useUsersStore().currentUser.credentials
const showImmediately = this.timeline.visibleStatuses.length === 0
window.addEventListener('scroll', this.handleScroll)
if (store.state.api.fetchers[this.timelineName]) {
return false
}
timelineFetcher.fetchAndUpdate({
credentials,
timeline: this.timelineName,
showImmediately,
userId: this.userId,
listId: this.listId,
statusId: this.statusId,
bookmarkFolderId: this.bookmarkFolderId,
tag: this.tag,
})
}, },
mounted() { mounted() {
if (document.hidden !== undefined) { if (document.hidden !== undefined) {
@ -165,6 +144,7 @@ const Timeline = {
this.unfocused = document.hidden this.unfocused = document.hidden
} }
window.addEventListener('keydown', this.handleShortKey) window.addEventListener('keydown', this.handleShortKey)
window.addEventListener('scroll', this.handleScroll)
setTimeout(this.determineVisibleStatuses, 250) setTimeout(this.determineVisibleStatuses, 250)
}, },
unmounted() { unmounted() {
@ -176,12 +156,14 @@ const Timeline = {
this.handleVisibilityChange, this.handleVisibilityChange,
false, false,
) )
useStatusesStore().setLoading({
timeline: this.timelineName,
value: false,
})
}, },
methods: { methods: {
timelineChange(newTimeline, oldTimeline) {
if (oldTimeline && oldTimeline.name !== 'friends') {
useTimelinesStore().clearTimeline(oldTimeline.name)
}
useTimelinesStore().startFetchingTimeline(newTimeline.name, newTimeline.argument)
},
stopBlockingClicks: debounce(function () { stopBlockingClicks: debounce(function () {
this.blockingClicks = false this.blockingClicks = false
}, 1000), }, 1000),
@ -198,52 +180,25 @@ const Timeline = {
}, },
showNewStatuses() { showNewStatuses() {
if (this.timeline.flushMarker !== 0) { if (this.timeline.flushMarker !== 0) {
useStatusesStore().clearTimeline({ useTimelinesStore().clearTimeline(this.timelineRef.name)
timeline: this.timelineName, useTimelinesStore().queueFlush(this.timelineRef.name, '')
excludeUserId: true,
})
useStatusesStore().queueFlush({ timeline: this.timelineName, id: 0 })
if (this.timelineName === 'user') {
this.$store.dispatch('fetchPinnedStatuses', this.userId)
}
this.fetchOlderStatuses() this.fetchOlderStatuses()
} else { } else {
this.blockClicksTemporarily() this.blockClicksTemporarily()
useStatusesStore().showNewStatuses(this.timelineName) useTimelinesStore().showNewStatuses(this.timelineRef.name)
this.paused = false this.paused = false
} }
window.scrollTo({ top: 0 }) window.scrollTo({ top: 0 })
}, },
fetchOlderStatuses: throttle( fetchOlderStatuses: throttle(
function () { function () {
const credentials = useUsersStore().currentUser.credentials this.timeline.fetcher
useStatusesStore().setLoading({ .fetchAndUpdate()
timeline: this.timelineName,
value: true,
})
timelineFetcher
.fetchAndUpdate({
credentials,
timeline: this.timelineName,
older: true,
showImmediately: true,
userId: this.userId,
listId: this.listId,
statusId: this.statusId,
bookmarkFolderId: this.bookmarkFolderId,
tag: this.tag,
})
.then(({ statuses }) => { .then(({ statuses }) => {
if (statuses?.length === 0) { if (statuses?.length === 0) {
this.bottomedOut = true this.bottomedOut = true
} }
}) })
.finally(() =>
useStatusesStore().setLoading({
timeline: this.timelineName,
value: false,
}),
)
}, },
1000, 1000,
this, this,
@ -314,6 +269,9 @@ const Timeline = {
}, },
}, },
watch: { watch: {
timelineRef(newTimeline, oldTimeline) {
this.timelineChange(newTimeline, oldTimeline)
},
filteredVisibleStatuses() { filteredVisibleStatuses() {
this.determineVisibleStatuses() this.determineVisibleStatuses()
}, },

View file

@ -9,7 +9,7 @@ import UserCard from 'src/components/user_card/user_card.vue'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js' import { useTimelinesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@ -44,14 +44,11 @@ const UserProfile = {
this.$store.dispatch('clearFriends', this.userId) this.$store.dispatch('clearFriends', this.userId)
}, },
computed: { computed: {
timeline() {
return this.$store.state.statuses.timelines.user
},
favorites() { favorites() {
return this.$store.state.statuses.timelines.favorites return useTimelinesStore().favorites
}, },
media() { media() {
return this.$store.state.statuses.timelines.media return useTimelinesStore().media
}, },
isUs() { isUs() {
return ( return (
@ -99,32 +96,12 @@ const UserProfile = {
}, },
fetchUsers(group) { fetchUsers(group) {
return () => return () =>
this.$store useUsersStore()['fetch' + group](this.userId)
.dispatch('fetch' + group, this.userId)
.then((result) => ({ items: result })) .then((result) => ({ items: result }))
}, },
load(userNameOrId) { load(userNameOrId) {
const startFetchingTimeline = (timeline, userId) => {
// Clear timeline only if load another user's profile
if (userId !== this.$store.state.statuses.timelines[timeline].userId) {
useStatusesStore().clearTimeline({ timeline: 'user' })
useStatusesStore().clearTimeline({ timeline: 'userPinned' })
useStatusesStore().clearTimeline({ timeline: 'media' })
}
this.$store.dispatch('startFetchingTimeline', { timeline, userId })
}
const loadById = (userId) => { const loadById = (userId) => {
this.userId = userId this.userId = userId
startFetchingTimeline('user', userId)
startFetchingTimeline('media', userId)
if (this.isUs) {
startFetchingTimeline('favorites')
} else if (!this.user.hide_favorites) {
startFetchingTimeline('favorites', userId)
}
// Fetch all pinned statuses immediately
this.$store.dispatch('fetchPinnedStatuses', userId)
} }
// Reset view // Reset view
@ -138,6 +115,7 @@ const UserProfile = {
const user = maybeId const user = maybeId
? useUsersStore().findUser(maybeId) ? useUsersStore().findUser(maybeId)
: useUsersStore().findUserByName(maybeName) : useUsersStore().findUserByName(maybeName)
if (user) { if (user) {
loadById(user.id) loadById(user.id)
} else { } else {
@ -159,13 +137,7 @@ const UserProfile = {
}) })
} }
}, },
stopFetching() {
this.$store.dispatch('stopFetchingTimeline', 'user')
this.$store.dispatch('stopFetchingTimeline', 'favorites')
this.$store.dispatch('stopFetchingTimeline', 'media')
},
switchUser(userNameOrId) { switchUser(userNameOrId) {
this.stopFetching()
this.load(userNameOrId) this.load(userNameOrId)
}, },
onTabSwitch(tab) { onTabSwitch(tab) {

View file

@ -8,7 +8,6 @@
<UserCard <UserCard
:user-id="userId" :user-id="userId"
:switcher="true" :switcher="true"
:selected="timeline.viewing"
:compact="compactProfiles" :compact="compactProfiles"
avatar-action="zoom" avatar-action="zoom"
:has-note-editor="true" :has-note-editor="true"
@ -19,19 +18,21 @@
:render-only-focused="true" :render-only-focused="true"
:on-switch="onTabSwitch" :on-switch="onTabSwitch"
> >
<Timeline <div
key="statuses" class="statuses"
:label="$t('user_card.statuses')" :label="$t('user_card.statuses')"
:count="user.statuses_count" :count="user.statuses_count"
:embedded="true"
:title="$t('user_profile.timeline_title')" :title="$t('user_profile.timeline_title')"
:timeline="timeline" >
timeline-name="user" <Timeline
:user-id="userId" key="statuses"
:pinned-status-ids="user.pinnedStatusIds" :embedded="true"
:in-profile="true" timeline-name="user"
:footer-slipgate="footerRef" :argument="userId"
/> :in-profile="true"
:footer-slipgate="footerRef"
/>
</div>
<div <div
v-if="followsTabVisible" v-if="followsTabVisible"
key="followees" key="followees"
@ -74,8 +75,7 @@
:embedded="true" :embedded="true"
:title="$t('user_card.media')" :title="$t('user_card.media')"
timeline-name="media" timeline-name="media"
:timeline="media" :argument="userId"
:user-id="userId"
:in-profile="true" :in-profile="true"
:footer-slipgate="footerRef" :footer-slipgate="footerRef"
/> />
@ -87,8 +87,7 @@
:embedded="true" :embedded="true"
:title="$t('user_card.favorites')" :title="$t('user_card.favorites')"
timeline-name="favorites" timeline-name="favorites"
:timeline="favorites" :argument="isUs ? undefined : userId"
:user-id="isUs ? undefined : userId"
:in-profile="true" :in-profile="true"
:footer-slipgate="footerRef" :footer-slipgate="footerRef"
/> />

View file

@ -10,7 +10,6 @@ import { useOAuthStore } from 'src/stores/oauth.js'
import { useShoutStore } from 'src/stores/shout.js' import { useShoutStore } from 'src/stores/shout.js'
import { useStatusesStore } from 'src/stores/statuses.js' import { useStatusesStore } from 'src/stores/statuses.js'
import { fetchTimeline } from 'src/api/timelines.js'
import { import {
getMastodonSocketURI, getMastodonSocketURI,
ProcessedWS, ProcessedWS,
@ -18,7 +17,6 @@ import {
} from 'src/api/websocket.js' } from 'src/api/websocket.js'
import followRequestFetcher from 'src/services/follow_request_fetcher/follow_request_fetcher.service' import followRequestFetcher from 'src/services/follow_request_fetcher/follow_request_fetcher.service'
import notificationsFetcher from 'src/services/notifications_fetcher/notifications_fetcher.service.js' import notificationsFetcher from 'src/services/notifications_fetcher/notifications_fetcher.service.js'
import timelineFetcher from 'src/services/timeline_fetcher/timeline_fetcher.service.js'
const retryTimeout = (multiplier) => 1000 * multiplier const retryTimeout = (multiplier) => 1000 * multiplier
@ -240,53 +238,6 @@ const api = {
state.mastoUserSocket.close() state.mastoUserSocket.close()
}, },
// Timelines
startFetchingTimeline(
store,
{
timeline = 'friends',
tag = false,
userId = false,
listId = false,
statusId = false,
bookmarkFolderId = false,
},
) {
if (
timeline === 'favourites' &&
!useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable
)
return
if (store.state.fetchers[timeline]) return
const fetcher = timelineFetcher.startFetching({
timeline,
store,
userId,
listId,
statusId,
bookmarkFolderId,
tag,
credentials: useOAuthStore().token,
})
store.commit('addFetcher', { fetcherName: timeline, fetcher })
},
stopFetchingTimeline(store, timeline) {
const fetcher = store.state.fetchers[timeline]
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: timeline, fetcher })
},
fetchTimeline(store, { timeline, ...rest }) {
fetchTimeline({
store,
timeline,
...rest,
credentials: useOAuthStore().token,
})
},
// Notifications // Notifications
startFetchingNotifications(store) { startFetchingNotifications(store) {
if (store.state.fetchers.notifications) return if (store.state.fetchers.notifications) return

View file

@ -1,6 +1,9 @@
import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js' import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js'
import { muteFilterHits } from '../status_parser/status_parser.js' import { muteFilterHits } from '../status_parser/status_parser.js'
import { prepareNotificationObject, isStatusNotification } from './notification_utils_sw.js' import {
isStatusNotification,
prepareNotificationObject,
} from './notification_utils_sw.js'
import { useNotificationsStore } from 'src/stores/notifications.js' import { useNotificationsStore } from 'src/stores/notifications.js'

View file

@ -6,94 +6,81 @@ import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.j
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.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 { useUsersStore } from 'src/stores/users.js'
import { fetchTimeline } from 'src/api/timelines.js' import { fetchTimeline } from 'src/api/timelines.js'
const update = ({ const REPLY_VISIBILITY_TIMELINES = new Set([
statuses, 'friends',
timeline, 'public',
showImmediately, 'publicAndExternal',
userId, 'bubble',
listId, ])
pagination,
}) => {
const ccTimeline = camelCase(timeline)
useStatusesStore().addNewStatuses({
timelineName: ccTimeline,
userId,
listId,
statuses,
showImmediately,
pagination,
})
}
const fetchAndUpdate = ({ const fetchAndUpdate = ({
timeline,
argument,
credentials, credentials,
timeline = 'friends', }, {
older = false,
showImmediately = false,
userId,
listId,
statusId,
bookmarkFolderId,
tag,
maxId, maxId,
sinceId, sinceId,
older = false,
showImmediately = false,
}) => { }) => {
const args = { timeline, credentials } timeline.loading = true
const timelineData = useStatusesStore().timelines[camelCase(timeline)]
const { hideMutedPosts, replyVisibility } = const { hideMutedPosts, replyVisibility } =
useMergedConfigStore().mergedConfig useMergedConfigStore().mergedConfig
const loggedIn = !!useUsersStore().currentUser const loggedIn = useUsersStore().loggedIn
const args = { timeline: timeline.name, credentials }
args[ARGUMENT_MAP[timeline.name]] = argument
if (older) { if (older) {
// When minId = 0 we need to fetch without maxId param // When minId = 0 we need to fetch without maxId param
args.maxId = maxId || timelineData.minId || null args.maxId = maxId || timeline.minId || null
} else { } else {
if (sinceId === undefined) { if (sinceId === undefined) {
args.sinceId = timelineData.maxId args.sinceId = timeline.maxId
} else if (sinceId !== null) { } else if (sinceId !== null) {
args.sinceId = sinceId args.sinceId = sinceId
} }
} }
args.userId = userId
args.listId = listId
args.statusId = statusId
args.bookmarkFolderId = bookmarkFolderId
args.tag = tag
args.withMuted = !hideMutedPosts args.withMuted = !hideMutedPosts
if ( if (loggedIn && REPLY_VISIBILITY_TIMELINES.has(timeline)) {
loggedIn &&
['friends', 'public', 'publicAndExternal', 'bubble'].includes(timeline)
) {
args.replyVisibility = replyVisibility args.replyVisibility = replyVisibility
} }
const numStatusesBeforeFetch = timelineData.statuses.length const numStatusesBeforeFetch = timeline.statuses.size
return fetchTimeline(args) return fetchTimeline(args)
.then((response) => { .then((response) => {
const { data: statuses, pagination } = response const { data: statuses, pagination, timestamp } = response
if ( if (
!older && !older &&
statuses.length >= 20 && statuses.length >= 20 &&
!timelineData.loading && !timeline.loading &&
numStatusesBeforeFetch > 0 numStatusesBeforeFetch > 0
) { ) {
useStatusesStore().queueFlush({ timeline, id: timelineData.maxId }) useTimelinesStore().queueFlush(timeline.name, timeline.maxId)
} }
update({
statuses, const processed = useStatusesStore()
timeline, .addNewStatuses({ statuses, timestamp })
showImmediately, .filter(Boolean)
userId,
listId, console.log(timeline.name, argument, showImmediately, processed.length)
pagination,
}) useTimelinesStore().addStatusesToTimeline(
timeline.name,
argument,
{
statuses,
showImmediately,
pagination,
}
)
return { statuses, pagination } return { statuses, pagination }
}) })
.catch((error) => { .catch((error) => {
@ -108,48 +95,53 @@ const fetchAndUpdate = ({
timeout: 5000, timeout: 5000,
}) })
}) })
.finally(() => {
timeline.loading = false
})
} }
const startFetching = ({ const timelineFetcher = (timeline, argument, argumentKey, credentials) => {
timeline = 'friends', const state = {
credentials, interval: null
userId, }
listId,
statusId, const boundFetchAndUpdate = ({
bookmarkFolderId, showImmediately,
tag, maxId,
}) => { sinceId,
const timelineData = useStatusesStore().timelines[camelCase(timeline)] older,
const showImmediately = timelineData.visibleStatuses.size === 0 } = {}) => fetchAndUpdate({
console.log(timeline) timeline,
timelineData.userId = userId argument,
timelineData.listId = listId argumentKey,
timelineData.bookmarkFolderId = bookmarkFolderId credentials,
fetchAndUpdate({ }, {
timeline, maxId,
credentials, sinceId,
older,
showImmediately, showImmediately,
userId,
listId,
statusId,
bookmarkFolderId,
tag,
}) })
const boundFetchAndUpdate = () =>
fetchAndUpdate({ const startFetching = () => {
timeline, if (state.interval) throw new Error('Interval already exists!')
credentials,
userId, boundFetchAndUpdate({
listId, showImmediately: timeline.visibleStatusesIds.size === 0
statusId,
bookmarkFolderId,
tag,
}) })
return promiseInterval(boundFetchAndUpdate, 10000)
} state.interval = promiseInterval(boundFetchAndUpdate, 10000)
const timelineFetcher = { }
fetchAndUpdate,
startFetching, const stopFetching = () => {
state.interval.stop()
state.interval = null
}
return {
startFetching,
stopFetching,
fetchAndUpdate: boundFetchAndUpdate,
}
} }
export default timelineFetcher export default timelineFetcher

View file

@ -8,9 +8,7 @@ import {
isValidNotification, isValidNotification,
maybeShowNotification, maybeShowNotification,
} from '../services/notification_utils/notification_utils.js' } from '../services/notification_utils/notification_utils.js'
import { import { isStatusNotification } from '../services/notification_utils/notification_utils_sw.js'
isStatusNotification,
} from '../services/notification_utils/notification_utils_sw.js'
import { useI18nStore } from 'src/stores/i18n.js' import { useI18nStore } from 'src/stores/i18n.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'

View file

@ -1,4 +1,3 @@
import { first, last, maxBy, minBy } from 'lodash'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
@ -33,42 +32,12 @@ import {
unretweet, unretweet,
} from 'src/api/user.js' } from 'src/api/user.js'
const emptyTl = (userId) => ({
statuses: new Map(),
faves: [],
visibleStatuses: new Map(),
newStatusCount: 0,
maxId: '',
minId: '',
minVisibleId: 0,
loading: false,
followers: [],
friends: [],
userId,
flushMarker: 0,
})
export const defaultState = () => ({ export const defaultState = () => ({
allStatuses: new Map(), allStatuses: new Map(),
timestamps: new WeakMap(), timestamps: new WeakMap(),
scrobblesNextFetch: {}, scrobblesNextFetch: {},
conversations: new Map(), conversations: new Map(),
favorites: new Set(), favorites: new Set(),
timelines: {
mentions: emptyTl(),
public: emptyTl(),
user: emptyTl(),
userPinned: emptyTl(),
favorites: emptyTl(),
media: emptyTl(),
publicAndExternal: emptyTl(),
friends: emptyTl(),
tag: emptyTl(),
dms: emptyTl(),
bookmarks: emptyTl(),
list: emptyTl(),
bubble: emptyTl(),
},
}) })
const getLatestScrobble = (user) => { const getLatestScrobble = (user) => {
@ -106,40 +75,19 @@ const getLatestScrobble = (user) => {
}) })
} }
const USER_TIMELINES = new Set(['user', 'userPinned', 'media'])
export const useStatusesStore = defineStore('statuses', { export const useStatusesStore = defineStore('statuses', {
state: defaultState, state: defaultState,
actions: { actions: {
addNewStatuses({ addNewStatuses({ statuses, user = {}, userId, timestamp }) {
statuses,
showImmediately = false,
timelineName,
user = {},
userId,
noIdUpdate = false,
pagination = {},
timestamp,
}) {
// Sanity check // Sanity check
if (!Array.isArray(statuses)) { if (!Array.isArray(statuses)) {
return false throw new TypeError("Statuses aren't an array!")
} }
const timeline = this.timelines[timelineName] // addStatus should always return "main" status,
// not "sub-status" i.e. retweeted/quoted/liked status
if (timeline && !noIdUpdate && statuses.length > 0) { // in case of likes (which are not statuses) it should return null
this.updateTimelineExtremes(timeline, statuses, pagination) const addStatus = (data) => {
}
// This makes sure that user timeline won't get data meant for other
// user. I.e. opening different user profiles makes request which could
// return data late after user already viewing different user profile
if (USER_TIMELINES.has(timelineName) && timeline.userId !== userId) {
return
}
const addStatus = (data, showImmediately, addToTimeline = true) => {
getLatestScrobble(data.user) getLatestScrobble(data.user)
const [status] = this.mergeOrAdd(this.allStatuses, data) const [status] = this.mergeOrAdd(this.allStatuses, data)
@ -154,49 +102,9 @@ export const useStatusesStore = defineStore('statuses', {
conversations.set(conversationId, new Map([[status.id, status]])) conversations.set(conversationId, new Map([[status.id, status]]))
} }
// We are mentioned in a post // Work on quote
if (
status.type === 'status' &&
status.attentions.some(({ id }) => id === user.id)
) {
const mentions = this.timelines.mentions
// Add the mention to the mentions timeline
if (timeline !== mentions) {
const [, isNew] = this.mergeOrAdd(mentions.statuses, data)
if (isNew) mentions.newStatusCount += 1
}
}
if (status.visibility === 'direct') {
const dms = this.timelines.dms
const [, isNew] = this.mergeOrAdd(dms.statuses, data)
if (isNew) dms.newStatusCount += 1
}
// Some statuses should only be added to the global status repository.
if (timeline && addToTimeline) {
// Decide if we should treat the status as new for this timeline.
const [status, isNew] = this.mergeOrAdd(timeline.statuses, data)
if (isNew) {
if (showImmediately) {
// Add it directly to the visibleStatuses, don't change
// newStatusCount
timeline.visibleStatuses.set(status.id, status)
} else {
// Just change newStatuscount
timeline.newStatusCount += 1
}
}
}
if (status.quote) { if (status.quote) {
addStatus( status.quote = addStatus(status.quote)
status.quote,
/* showImmediately = */ false,
/* addToTimeline = */ false,
)
} }
return status return status
@ -204,41 +112,15 @@ export const useStatusesStore = defineStore('statuses', {
const processors = { const processors = {
status: (status) => { status: (status) => {
addStatus(status, showImmediately) return addStatus(status)
}, },
edit: (status) => { edit: (status) => {
addStatus(status, showImmediately) return addStatus(status)
}, },
retweet: (status) => { retweet: (status) => {
// RetweetedStatuses are never shown immediately // RetweetedStatuses are never shown immediately
const retweetedStatus = addStatus( if (status.retweeted_status) addStatus(status.retweeted_status)
status.retweeted_status, return addStatus(status)
false,
false,
)
let retweet
// If the retweeted status is already there, don't add the retweet
// to the timeline.
if (
[...(timeline?.statuses.values() ?? [])].some((s) => {
if (s.retweeted_status) {
return (
s.id === retweetedStatus.id ||
s.retweeted_status.id === retweetedStatus.id
)
} else {
return s.id === retweetedStatus.id
}
})
) {
// Already have it visible (either as the original or another RT), don't add to timeline, don't show.
retweet = addStatus(status, false, false)
} else {
retweet = addStatus(status, showImmediately)
}
retweet.retweeted_status = retweetedStatus
}, },
favorite: (favorite) => { favorite: (favorite) => {
// Only update if this is a new favorite. // Only update if this is a new favorite.
@ -258,19 +140,22 @@ export const useStatusesStore = defineStore('statuses', {
} }
return status return status
} }
return null
}, },
follow: () => { follow: () => {
// NOOP, it is known status but we don't do anything about it for now // NOOP, it is known status but we don't do anything about it for now
return null
}, },
default: (unknown) => { default: (unknown) => {
console.warn('unknown status type', unknown) console.warn('unknown status type', unknown)
return null
}, },
} }
statuses.forEach((status) => { return statuses.map((status) => {
const type = status.type const type = status.type
const processor = processors[type] ?? processors.default const processor = processors[type] ?? processors.default
processor(status) return processor(status)
}) })
}, },
mergeOrAdd(map, status, timestamp) { mergeOrAdd(map, status, timestamp) {
@ -307,21 +192,6 @@ export const useStatusesStore = defineStore('statuses', {
this.addNewStatuses({ statuses: [status], timestamp }), this.addNewStatuses({ statuses: [status], timestamp }),
) )
}, },
fetchPinnedStatuses(userId) {
return fetchPinnedStatuses({
id: userId,
credentials: useOAuthStore().token,
}).then(({ data: statuses, timestamp }) =>
this.addNewStatuses({
statuses,
timeline: 'userPinned',
userId,
showImmediately: true,
noIdUpdate: true,
timestamp,
}),
)
},
fetchStatusSource(id) { fetchStatusSource(id) {
return fetchStatusSource({ return fetchStatusSource({
id, id,
@ -394,55 +264,6 @@ export const useStatusesStore = defineStore('statuses', {
status.emoji_reactions = emojiReactions status.emoji_reactions = emojiReactions
}, },
// Queues & Timeline manip
updateTimelineExtremes(timeline, statuses, pagination = {}) {
// Can't use Math.min/max because it doesn't work with string (duh)
const minNew = pagination.maxId ?? minBy(statuses, 'id').id ?? ''
const maxNew = pagination.minId ?? maxBy(statuses, 'id').id ?? ''
const newer = maxNew > timeline.maxId || timeline.maxId === ''
const older = minNew < timeline.minId || timeline.minId === ''
if (newer) {
timeline.maxId = maxNew
}
if (older) {
timeline.minId = minNew
}
},
showNewStatuses(timelineName) {
const timeline = this.timelines[timelineName]
timeline.newStatusCount = 0
timeline.visibleStatuses = new Map(
[...timeline.statuses.entries()].slice(0, 50),
)
timeline.minVisibleId = last(timeline.visibleStatuses.keys())
timeline.minId = ''
timeline.maxId = ''
this.updateTimelineExtremes(timeline, [...timeline.statuses.values()])
},
resetStatuses() {
const emptyState = defaultState()
Object.entries(emptyState).forEach(([key, value]) => {
this[key] = value
})
},
clearTimeline({ timeline, excludeUserId = false }) {
const userId = excludeUserId ? this.timelines[timeline].userId : undefined
this.timelines[timeline] = emptyTl(userId)
},
queueFlush({ timeline, id }) {
this.timelines[timeline].flushMarker = id
},
queueFlushAll() {
Object.keys(this.timelines).forEach((timeline) => {
this.timelines[timeline].flushMarker = this.timelines[timeline].maxId
})
},
// Actions // Actions
/// Favorite /// Favorite
favorite(id) { favorite(id) {
@ -770,25 +591,6 @@ export const useStatusesStore = defineStore('statuses', {
return data return data
}) })
}, },
// Misc
removeUserStatuses({ timelineName, userId }) {
const timeline = this.timelines[timelineName]
timeline.statuses
.values()
.filter(({ user }) => user.id === userId)
.forEach(({ id }) => {
timeline.statuses.delete(id)
timeline.visibleStatuses.delete(id)
})
timeline.minVisibleId =
timeline.visibleStatuses.length > 0
? last(timeline.visibleStatuses).id
: 0
timeline.maxId =
timeline.statuses.length > 0 ? first(timeline.statuses).id : 0
},
setVirtualHeight({ statusId, height }) { setVirtualHeight({ statusId, height }) {
this.allStatuses.get(statusId).virtualHeight = height this.allStatuses.get(statusId).virtualHeight = height
}, },
@ -796,8 +598,5 @@ export const useStatusesStore = defineStore('statuses', {
const status = this.allStatuses.get(id) const status = this.allStatuses.get(id)
status.poll = poll status.poll = poll
}, },
setLoading({ timeline, value }) {
this.timelines[timeline].loading = value
},
}, },
}) })

233
src/stores/timelines.js Normal file
View file

@ -0,0 +1,233 @@
import { first, last, max, min } from 'lodash'
import { defineStore } from 'pinia'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useUsersStore } from 'src/stores/users.js'
import timelineFetcher from 'src/services/timeline_fetcher/timeline_fetcher.service.js'
const emptyTl = (name, argument = null) => {
const result = {
name,
statuses: new Map(),
visibleStatusesIds: new Set(),
newStatusCount: 0,
maxId: 0,
minId: 0,
minVisibleId: 0,
loading: false,
flushMarker: 0,
fetcher: null,
}
const property = USER_TIMELINES.has(name) ? 'userId' : ARGUMENT_MAP[name]
if (property) {
result[property] = argument
}
return result
}
export const ARGUMENT_MAP = {
tag: 'tag',
list: 'listId',
bookmarks: 'bookmarkFolderId',
quotes: 'statusId',
search: 'query',
}
export const defaultState = () => {
return Object.fromEntries([
'mentions',
'public',
'user',
'userPinned',
'media',
'favorites',
'publicAndExternal',
'friends',
'tag',
'dms',
'bookmarks',
'list',
'bubble',
'quotes',
'search',
].map((name) => [name, emptyTl(name)]))
}
const USER_TIMELINES = new Set(['user', 'userPinned', 'media', 'favorites'])
//const CUSTOM_SORT = new Set(['bookmarks', 'favorites'])
export const useTimelinesStore = defineStore('timelines', {
state: defaultState,
actions: {
addStatusesToTimeline(
timelineName,
argument,
{
statuses,
showImmediately = false,
noIdUpdate = false,
pagination = {},
nested = false,
},
) {
if (statuses.length === 0) return
const timeline = this[timelineName]
// This makes sure that user timeline won't get data meant for other
// user. I.e. opening different user profiles makes request which could
// return data late after user already viewing different user profile
// Same can happen with tags etc.
const property = USER_TIMELINES.has(name) ? 'userId' : ARGUMENT_MAP[name]
if (property && timeline[property] !== argument) {
return
}
if (!noIdUpdate) {
this.updateTimelineExtremes(
timeline,
statuses.map((x) => x.id),
pagination,
)
}
statuses.forEach((status) => {
const isNew = !timeline.statuses.has(status.id)
timeline.statuses.set(status.id, status)
if (isNew) {
if (showImmediately) {
// Add it directly to the visibleStatuses, don't change
// newStatusCount
timeline.visibleStatusesIds.add(status.id)
} else {
// Just change newStatuscount
timeline.newStatusCount += 1
}
}
if (nested) return
// We are mentioned in a post
if (
status.type === 'status' &&
status.attentions.some(
({ id }) => id === useUsersStore().currentUser?.id,
)
) {
// Add the mention to the mentions timeline
if (timeline !== this.mentions) {
this.addStatusesToTimeline('mentions', null, { statuses, nested: true })
}
}
if (status.visibility === 'direct') {
if (timeline !== this.dms) {
this.addStatusesToTimeline('dms', null, { statuses, nested: true })
}
}
})
},
// Fetchers
startFetchingTimeline(timelineName, argument) {
const timeline = this[timelineName]
if (timeline.fetcher) return
if (
timelineName === 'favourites' &&
!useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable
) {
return
}
timeline.fetcher = timelineFetcher(
timeline,
argument,
ARGUMENT_MAP[timeline.name],
useOAuthStore().token,
)
timeline.fetcher.startFetching()
},
stopFetchingTimeline(timelineName) {
const timeline = this[timelineName]
timeline.fetcher?.stopFetching()
timeline.fetcher = null
},
// Queues & Timeline manip
updateTimelineExtremes(timeline, statuses, pagination = {}) {
// Can't use Math.min/max because it doesn't work with string (duh)
const minNew = pagination.maxId ?? min(...statuses) ?? ''
const maxNew = pagination.minId ?? max(...statuses) ?? ''
const newer = maxNew > timeline.maxId || timeline.maxId === ''
const older = minNew < timeline.minId || timeline.minId === ''
if (newer) {
timeline.maxId = maxNew
}
if (older) {
timeline.minId = minNew
}
},
resetStatuses() {
const emptyState = defaultState()
Object.entries(emptyState).forEach(([key, value]) => {
this[key] = value
})
},
showNewStatuses(timelineName) {
const timeline = this[timelineName]
timeline.newStatusCount = 0
timeline.visibleStatusesIds = new Set(
[...timeline.statuses.keys()].slice(0, 50),
)
timeline.minVisibleId = last(timeline.visibleStatusesIds.keys())
timeline.minId = ''
timeline.maxId = ''
this.updateTimelineExtremes(timeline, [...timeline.statuses.keys()])
},
clearTimeline(timeline, excludeUserId = false) {
const userId = excludeUserId ? this[timeline].userId : undefined
this.stopFetchingTimeline(timeline)
this[timeline] = emptyTl(timeline, userId)
},
queueFlush(timeline, id) {
this[timeline].flushMarker = id
},
queueFlushAll() {
Object.keys(this).forEach((timeline) => {
this[timeline].flushMarker = this[timeline].maxId
})
},
// Misc
removeUserStatuses({ timelineName, userId }) {
const timeline = this.timelines[timelineName]
timeline.statuses
.values()
.filter(({ user }) => user.id === userId)
.forEach(({ id }) => {
timeline.statuses.delete(id)
timeline.visibleStatusesIds.delete(id)
})
timeline.minVisibleId =
timeline.visibleStatusesIds.size > 0
? last(timeline.visibleStatusesIds).id
: 0
timeline.maxId =
timeline.statuses.length > 0 ? first(timeline.statuses).id : 0
},
},
})

View file

@ -23,6 +23,7 @@ import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useNotificationsStore } from 'src/stores/notifications.js' import { useNotificationsStore } from 'src/stores/notifications.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useStatusesStore } from 'src/stores/statuses.js' import { useStatusesStore } from 'src/stores/statuses.js'
import { useTimelinesStore } from 'src/stores/timelines.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUserHighlightStore } from 'src/stores/user_highlight.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js'
@ -667,7 +668,7 @@ export const useUsersStore = defineStore('users', {
const startPolling = () => { const startPolling = () => {
// Start getting fresh posts. // Start getting fresh posts.
dispatch('startFetchingTimeline', { timeline: 'friends' }) useTimelinesStore().startFetchingTimeline('friends')
// Start fetching notifications // Start fetching notifications
dispatch('startFetchingNotifications') dispatch('startFetchingNotifications')