diff --git a/src/boot/routes.js b/src/boot/routes.js
index 3b21d874d..8f8335c9c 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -1,10 +1,7 @@
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 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 TagTimeline from 'src/components/tag_timeline/tag_timeline.vue'
import Timeline from 'src/components/timeline/timeline.vue'
import { useInstanceStore } from 'src/stores/instance.js'
@@ -40,7 +37,7 @@ export default (store) => {
path: '/main/all',
component: Timeline,
props: () => ({
- timelineName: 'publicAndExternal',
+ timelineRef: { name: 'publicAndExternal' },
}),
},
{
@@ -48,7 +45,7 @@ export default (store) => {
path: '/main/public',
component: Timeline,
props: () => ({
- timelineName: 'public',
+ timelineRef: { name: 'public' },
}),
},
{
@@ -57,17 +54,31 @@ export default (store) => {
component: Timeline,
beforeEnter: validateAuthenticatedRoute,
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',
path: '/bubble',
component: Timeline,
props: () => ({
- timelineName: 'bubble',
+ timelineRef: { name: 'bubble' },
}),
},
{
@@ -84,7 +95,14 @@ export default (store) => {
meta: { dontScroll: true },
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',
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
@@ -120,7 +138,7 @@ export default (store) => {
component: Timeline,
beforeEnter: validateAuthenticatedRoute,
props: () => ({
- timelineName: 'dms',
+ timelineRef: { name: 'dms' },
}),
},
{
@@ -218,8 +236,10 @@ export default (store) => {
{
name: 'lists-timeline',
path: '/lists/:id',
- component: () =>
- import('src/components/lists_timeline/lists_timeline.vue'),
+ component: Timeline,
+ props: (route) => ({
+ timelineRef: { name: 'lists', argument: route.params.id },
+ }),
},
{
name: 'lists-edit',
@@ -253,7 +273,10 @@ export default (store) => {
{
name: 'bookmark-folder',
path: '/bookmarks/:id',
- component: BookmarkTimeline,
+ component: Timeline,
+ props: (route) => ({
+ timelineRef: { name: 'bookmarks', argument: route.params.id },
+ }),
},
{
name: 'bookmark-folder-edit',
diff --git a/src/components/bookmark_timeline/bookmark_timeline.js b/src/components/bookmark_timeline/bookmark_timeline.js
deleted file mode 100644
index dbda08ba1..000000000
--- a/src/components/bookmark_timeline/bookmark_timeline.js
+++ /dev/null
@@ -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
diff --git a/src/components/bookmark_timeline/bookmark_timeline.vue b/src/components/bookmark_timeline/bookmark_timeline.vue
deleted file mode 100644
index cc86cf12e..000000000
--- a/src/components/bookmark_timeline/bookmark_timeline.vue
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
diff --git a/src/components/bubble_timeline/bubble_timeline.js b/src/components/bubble_timeline/bubble_timeline.js
deleted file mode 100644
index fcbc31ad2..000000000
--- a/src/components/bubble_timeline/bubble_timeline.js
+++ /dev/null
@@ -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
diff --git a/src/components/lists_timeline/lists_timeline.js b/src/components/lists_timeline/lists_timeline.js
deleted file mode 100644
index ae58e58ed..000000000
--- a/src/components/lists_timeline/lists_timeline.js
+++ /dev/null
@@ -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
diff --git a/src/components/lists_timeline/lists_timeline.vue b/src/components/lists_timeline/lists_timeline.vue
deleted file mode 100644
index 18156b812..000000000
--- a/src/components/lists_timeline/lists_timeline.vue
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
diff --git a/src/components/mentions/mentions.js b/src/components/mentions/mentions.js
deleted file mode 100644
index 48e06a950..000000000
--- a/src/components/mentions/mentions.js
+++ /dev/null
@@ -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
diff --git a/src/components/mentions/mentions.vue b/src/components/mentions/mentions.vue
deleted file mode 100644
index 70f60bafe..000000000
--- a/src/components/mentions/mentions.vue
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
diff --git a/src/components/quotes_timeline/quotes_timeline.js b/src/components/quotes_timeline/quotes_timeline.js
deleted file mode 100644
index 4778f1daa..000000000
--- a/src/components/quotes_timeline/quotes_timeline.js
+++ /dev/null
@@ -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
diff --git a/src/components/quotes_timeline/quotes_timeline.vue b/src/components/quotes_timeline/quotes_timeline.vue
deleted file mode 100644
index 835abd12e..000000000
--- a/src/components/quotes_timeline/quotes_timeline.vue
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
diff --git a/src/components/tag_timeline/tag_timeline.js b/src/components/tag_timeline/tag_timeline.js
deleted file mode 100644
index da3fcb693..000000000
--- a/src/components/tag_timeline/tag_timeline.js
+++ /dev/null
@@ -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
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index e3ba35e07..040ae80e7 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -10,10 +10,9 @@ import TimelineMenu from 'src/components/timeline_menu/timeline_menu.vue'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
+import { useTimelinesStore } from 'src/stores/timelines.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 {
faArrowUp,
@@ -28,12 +27,8 @@ library.add(faCircleNotch, faCog, faMinus, faArrowUp, faCirclePlus, faCheck)
const Timeline = {
props: {
- timelineName: String,
- userId: String,
- listId: String,
- statusId: String,
- bookmarkFolderId: String,
- tag: String,
+ timelineRef: Object,
+ argument: String,
embedded: Boolean,
count: Number,
pinnedStatusIds: Set,
@@ -59,15 +54,16 @@ const Timeline = {
},
computed: {
timeline() {
- return useStatusesStore().timelines[this.timelineName]
+ return useTimelinesStore()[this.timelineRef.name]
},
filteredVisibleStatuses() {
- return [...this.timeline.visibleStatuses.values()].filter(
- (status) =>
- this.timelineName !== 'user' ||
- (status.id >= this.timeline.minId &&
- status.id <= this.timeline.maxId),
- )
+ return [...this.timeline.visibleStatusesIds.keys()]
+ .filter(
+ (id) =>
+ this.timelineRef.name !== 'user' ||
+ (id >= this.timeline.minId && id <= this.timeline.maxId),
+ )
+ .map((id) => this.timeline.statuses.get(id))
},
filteredPinnedStatusIds() {
return (this.pinnedStatusIds || []).filter(
@@ -118,13 +114,15 @@ const Timeline = {
return keyBy(this.pinnedStatusIds)
},
statusesToDisplay() {
- const amount = this.timeline.visibleStatuses.size
+ const amount = this.timeline.visibleStatusesIds.size
const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80))
const nonPinnedIndex =
this.virtualScrollIndex - this.filteredPinnedStatusIds.length
const min = Math.max(0, 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() {
return useMergedConfigStore().mergedConfig.virtualScrolling
@@ -134,26 +132,7 @@ const Timeline = {
}),
},
created() {
- const store = this.$store
- 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,
- })
+ this.timelineChange(this.timelineRef)
},
mounted() {
if (document.hidden !== undefined) {
@@ -165,6 +144,7 @@ const Timeline = {
this.unfocused = document.hidden
}
window.addEventListener('keydown', this.handleShortKey)
+ window.addEventListener('scroll', this.handleScroll)
setTimeout(this.determineVisibleStatuses, 250)
},
unmounted() {
@@ -176,12 +156,14 @@ const Timeline = {
this.handleVisibilityChange,
false,
)
- useStatusesStore().setLoading({
- timeline: this.timelineName,
- value: false,
- })
},
methods: {
+ timelineChange(newTimeline, oldTimeline) {
+ if (oldTimeline && oldTimeline.name !== 'friends') {
+ useTimelinesStore().clearTimeline(oldTimeline.name)
+ }
+ useTimelinesStore().startFetchingTimeline(newTimeline.name, newTimeline.argument)
+ },
stopBlockingClicks: debounce(function () {
this.blockingClicks = false
}, 1000),
@@ -198,52 +180,25 @@ const Timeline = {
},
showNewStatuses() {
if (this.timeline.flushMarker !== 0) {
- useStatusesStore().clearTimeline({
- timeline: this.timelineName,
- excludeUserId: true,
- })
- useStatusesStore().queueFlush({ timeline: this.timelineName, id: 0 })
- if (this.timelineName === 'user') {
- this.$store.dispatch('fetchPinnedStatuses', this.userId)
- }
+ useTimelinesStore().clearTimeline(this.timelineRef.name)
+ useTimelinesStore().queueFlush(this.timelineRef.name, '')
this.fetchOlderStatuses()
} else {
this.blockClicksTemporarily()
- useStatusesStore().showNewStatuses(this.timelineName)
+ useTimelinesStore().showNewStatuses(this.timelineRef.name)
this.paused = false
}
window.scrollTo({ top: 0 })
},
fetchOlderStatuses: throttle(
function () {
- const credentials = useUsersStore().currentUser.credentials
- useStatusesStore().setLoading({
- 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,
- })
+ this.timeline.fetcher
+ .fetchAndUpdate()
.then(({ statuses }) => {
if (statuses?.length === 0) {
this.bottomedOut = true
}
})
- .finally(() =>
- useStatusesStore().setLoading({
- timeline: this.timelineName,
- value: false,
- }),
- )
},
1000,
this,
@@ -314,6 +269,9 @@ const Timeline = {
},
},
watch: {
+ timelineRef(newTimeline, oldTimeline) {
+ this.timelineChange(newTimeline, oldTimeline)
+ },
filteredVisibleStatuses() {
this.determineVisibleStatuses()
},
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 49065c2a9..1b6b5f31c 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -9,7 +9,7 @@ import UserCard from 'src/components/user_card/user_card.vue'
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 { useTimelinesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core'
@@ -44,14 +44,11 @@ const UserProfile = {
this.$store.dispatch('clearFriends', this.userId)
},
computed: {
- timeline() {
- return this.$store.state.statuses.timelines.user
- },
favorites() {
- return this.$store.state.statuses.timelines.favorites
+ return useTimelinesStore().favorites
},
media() {
- return this.$store.state.statuses.timelines.media
+ return useTimelinesStore().media
},
isUs() {
return (
@@ -99,32 +96,12 @@ const UserProfile = {
},
fetchUsers(group) {
return () =>
- this.$store
- .dispatch('fetch' + group, this.userId)
+ useUsersStore()['fetch' + group](this.userId)
.then((result) => ({ items: result }))
},
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) => {
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
@@ -138,6 +115,7 @@ const UserProfile = {
const user = maybeId
? useUsersStore().findUser(maybeId)
: useUsersStore().findUserByName(maybeName)
+
if (user) {
loadById(user.id)
} 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) {
- this.stopFetching()
this.load(userNameOrId)
},
onTabSwitch(tab) {
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 6ca0c39bd..e7381937d 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -8,7 +8,6 @@
-
+ >
+
+
@@ -87,8 +87,7 @@
:embedded="true"
:title="$t('user_card.favorites')"
timeline-name="favorites"
- :timeline="favorites"
- :user-id="isUs ? undefined : userId"
+ :argument="isUs ? undefined : userId"
:in-profile="true"
:footer-slipgate="footerRef"
/>
diff --git a/src/modules/api.js b/src/modules/api.js
index e686df0fe..7a69e464e 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -10,7 +10,6 @@ import { useOAuthStore } from 'src/stores/oauth.js'
import { useShoutStore } from 'src/stores/shout.js'
import { useStatusesStore } from 'src/stores/statuses.js'
-import { fetchTimeline } from 'src/api/timelines.js'
import {
getMastodonSocketURI,
ProcessedWS,
@@ -18,7 +17,6 @@ import {
} from 'src/api/websocket.js'
import followRequestFetcher from 'src/services/follow_request_fetcher/follow_request_fetcher.service'
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
@@ -240,53 +238,6 @@ const api = {
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
startFetchingNotifications(store) {
if (store.state.fetchers.notifications) return
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index 5157504b2..1fdb7bec1 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -1,6 +1,9 @@
import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.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'
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index b7097f4e0..84c980556 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -6,94 +6,81 @@ import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.j
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 update = ({
- statuses,
- timeline,
- showImmediately,
- userId,
- listId,
- pagination,
-}) => {
- const ccTimeline = camelCase(timeline)
-
- useStatusesStore().addNewStatuses({
- timelineName: ccTimeline,
- userId,
- listId,
- statuses,
- showImmediately,
- pagination,
- })
-}
+const REPLY_VISIBILITY_TIMELINES = new Set([
+ 'friends',
+ 'public',
+ 'publicAndExternal',
+ 'bubble',
+])
const fetchAndUpdate = ({
+ timeline,
+ argument,
credentials,
- timeline = 'friends',
- older = false,
- showImmediately = false,
- userId,
- listId,
- statusId,
- bookmarkFolderId,
- tag,
+}, {
maxId,
sinceId,
+ older = false,
+ showImmediately = false,
}) => {
- const args = { timeline, credentials }
- const timelineData = useStatusesStore().timelines[camelCase(timeline)]
+ timeline.loading = true
const { hideMutedPosts, replyVisibility } =
useMergedConfigStore().mergedConfig
- const loggedIn = !!useUsersStore().currentUser
+ const loggedIn = useUsersStore().loggedIn
+
+ const args = { timeline: timeline.name, credentials }
+ args[ARGUMENT_MAP[timeline.name]] = argument
if (older) {
// When minId = 0 we need to fetch without maxId param
- args.maxId = maxId || timelineData.minId || null
+ args.maxId = maxId || timeline.minId || null
} else {
if (sinceId === undefined) {
- args.sinceId = timelineData.maxId
+ args.sinceId = timeline.maxId
} else if (sinceId !== null) {
args.sinceId = sinceId
}
}
- args.userId = userId
- args.listId = listId
- args.statusId = statusId
- args.bookmarkFolderId = bookmarkFolderId
- args.tag = tag
args.withMuted = !hideMutedPosts
- if (
- loggedIn &&
- ['friends', 'public', 'publicAndExternal', 'bubble'].includes(timeline)
- ) {
+ if (loggedIn && REPLY_VISIBILITY_TIMELINES.has(timeline)) {
args.replyVisibility = replyVisibility
}
- const numStatusesBeforeFetch = timelineData.statuses.length
+ const numStatusesBeforeFetch = timeline.statuses.size
return fetchTimeline(args)
.then((response) => {
- const { data: statuses, pagination } = response
+ const { data: statuses, pagination, timestamp } = response
if (
!older &&
statuses.length >= 20 &&
- !timelineData.loading &&
+ !timeline.loading &&
numStatusesBeforeFetch > 0
) {
- useStatusesStore().queueFlush({ timeline, id: timelineData.maxId })
+ useTimelinesStore().queueFlush(timeline.name, timeline.maxId)
}
- update({
- statuses,
- timeline,
- showImmediately,
- userId,
- listId,
- pagination,
- })
+
+ const processed = useStatusesStore()
+ .addNewStatuses({ statuses, timestamp })
+ .filter(Boolean)
+
+ console.log(timeline.name, argument, showImmediately, processed.length)
+
+ useTimelinesStore().addStatusesToTimeline(
+ timeline.name,
+ argument,
+ {
+ statuses,
+ showImmediately,
+ pagination,
+ }
+ )
return { statuses, pagination }
})
.catch((error) => {
@@ -108,48 +95,53 @@ const fetchAndUpdate = ({
timeout: 5000,
})
})
+ .finally(() => {
+ timeline.loading = false
+ })
}
-const startFetching = ({
- timeline = 'friends',
- credentials,
- userId,
- listId,
- statusId,
- bookmarkFolderId,
- tag,
-}) => {
- const timelineData = useStatusesStore().timelines[camelCase(timeline)]
- const showImmediately = timelineData.visibleStatuses.size === 0
- console.log(timeline)
- timelineData.userId = userId
- timelineData.listId = listId
- timelineData.bookmarkFolderId = bookmarkFolderId
- fetchAndUpdate({
- timeline,
- credentials,
+const timelineFetcher = (timeline, argument, argumentKey, credentials) => {
+ const state = {
+ interval: null
+ }
+
+ const boundFetchAndUpdate = ({
+ showImmediately,
+ maxId,
+ sinceId,
+ older,
+ } = {}) => fetchAndUpdate({
+ timeline,
+ argument,
+ argumentKey,
+ credentials,
+ }, {
+ maxId,
+ sinceId,
+ older,
showImmediately,
- userId,
- listId,
- statusId,
- bookmarkFolderId,
- tag,
})
- const boundFetchAndUpdate = () =>
- fetchAndUpdate({
- timeline,
- credentials,
- userId,
- listId,
- statusId,
- bookmarkFolderId,
- tag,
+
+ const startFetching = () => {
+ if (state.interval) throw new Error('Interval already exists!')
+
+ boundFetchAndUpdate({
+ showImmediately: timeline.visibleStatusesIds.size === 0
})
- return promiseInterval(boundFetchAndUpdate, 10000)
-}
-const timelineFetcher = {
- fetchAndUpdate,
- startFetching,
+
+ state.interval = promiseInterval(boundFetchAndUpdate, 10000)
+ }
+
+ const stopFetching = () => {
+ state.interval.stop()
+ state.interval = null
+ }
+
+ return {
+ startFetching,
+ stopFetching,
+ fetchAndUpdate: boundFetchAndUpdate,
+ }
}
export default timelineFetcher
diff --git a/src/stores/notifications.js b/src/stores/notifications.js
index c45f5b0a3..372700f11 100644
--- a/src/stores/notifications.js
+++ b/src/stores/notifications.js
@@ -8,9 +8,7 @@ import {
isValidNotification,
maybeShowNotification,
} from '../services/notification_utils/notification_utils.js'
-import {
- isStatusNotification,
-} from '../services/notification_utils/notification_utils_sw.js'
+import { isStatusNotification } from '../services/notification_utils/notification_utils_sw.js'
import { useI18nStore } from 'src/stores/i18n.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
diff --git a/src/stores/statuses.js b/src/stores/statuses.js
index 55060e251..699c2cf7c 100644
--- a/src/stores/statuses.js
+++ b/src/stores/statuses.js
@@ -1,4 +1,3 @@
-import { first, last, maxBy, minBy } from 'lodash'
import { defineStore } from 'pinia'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
@@ -33,42 +32,12 @@ import {
unretweet,
} 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 = () => ({
allStatuses: new Map(),
timestamps: new WeakMap(),
scrobblesNextFetch: {},
conversations: new Map(),
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) => {
@@ -106,40 +75,19 @@ const getLatestScrobble = (user) => {
})
}
-const USER_TIMELINES = new Set(['user', 'userPinned', 'media'])
-
export const useStatusesStore = defineStore('statuses', {
state: defaultState,
actions: {
- addNewStatuses({
- statuses,
- showImmediately = false,
- timelineName,
- user = {},
- userId,
- noIdUpdate = false,
- pagination = {},
- timestamp,
- }) {
+ addNewStatuses({ statuses, user = {}, userId, timestamp }) {
// Sanity check
if (!Array.isArray(statuses)) {
- return false
+ throw new TypeError("Statuses aren't an array!")
}
- const timeline = this.timelines[timelineName]
-
- if (timeline && !noIdUpdate && statuses.length > 0) {
- this.updateTimelineExtremes(timeline, statuses, pagination)
- }
-
- // 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) => {
+ // addStatus should always return "main" status,
+ // not "sub-status" i.e. retweeted/quoted/liked status
+ // in case of likes (which are not statuses) it should return null
+ const addStatus = (data) => {
getLatestScrobble(data.user)
const [status] = this.mergeOrAdd(this.allStatuses, data)
@@ -154,49 +102,9 @@ export const useStatusesStore = defineStore('statuses', {
conversations.set(conversationId, new Map([[status.id, status]]))
}
- // We are mentioned in a post
- 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
- }
- }
- }
-
+ // Work on quote
if (status.quote) {
- addStatus(
- status.quote,
- /* showImmediately = */ false,
- /* addToTimeline = */ false,
- )
+ status.quote = addStatus(status.quote)
}
return status
@@ -204,41 +112,15 @@ export const useStatusesStore = defineStore('statuses', {
const processors = {
status: (status) => {
- addStatus(status, showImmediately)
+ return addStatus(status)
},
edit: (status) => {
- addStatus(status, showImmediately)
+ return addStatus(status)
},
retweet: (status) => {
// RetweetedStatuses are never shown immediately
- const retweetedStatus = addStatus(
- status.retweeted_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
+ if (status.retweeted_status) addStatus(status.retweeted_status)
+ return addStatus(status)
},
favorite: (favorite) => {
// Only update if this is a new favorite.
@@ -258,19 +140,22 @@ export const useStatusesStore = defineStore('statuses', {
}
return status
}
+ return null
},
follow: () => {
// NOOP, it is known status but we don't do anything about it for now
+ return null
},
default: (unknown) => {
console.warn('unknown status type', unknown)
+ return null
},
}
- statuses.forEach((status) => {
+ return statuses.map((status) => {
const type = status.type
const processor = processors[type] ?? processors.default
- processor(status)
+ return processor(status)
})
},
mergeOrAdd(map, status, timestamp) {
@@ -307,21 +192,6 @@ export const useStatusesStore = defineStore('statuses', {
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) {
return fetchStatusSource({
id,
@@ -394,55 +264,6 @@ export const useStatusesStore = defineStore('statuses', {
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
/// Favorite
favorite(id) {
@@ -770,25 +591,6 @@ export const useStatusesStore = defineStore('statuses', {
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 }) {
this.allStatuses.get(statusId).virtualHeight = height
},
@@ -796,8 +598,5 @@ export const useStatusesStore = defineStore('statuses', {
const status = this.allStatuses.get(id)
status.poll = poll
},
- setLoading({ timeline, value }) {
- this.timelines[timeline].loading = value
- },
},
})
diff --git a/src/stores/timelines.js b/src/stores/timelines.js
new file mode 100644
index 000000000..5f78440fd
--- /dev/null
+++ b/src/stores/timelines.js
@@ -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
+ },
+ },
+})
diff --git a/src/stores/users.js b/src/stores/users.js
index 4aba00743..cf88bcd43 100644
--- a/src/stores/users.js
+++ b/src/stores/users.js
@@ -23,6 +23,7 @@ import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useNotificationsStore } from 'src/stores/notifications.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useStatusesStore } from 'src/stores/statuses.js'
+import { useTimelinesStore } from 'src/stores/timelines.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUserHighlightStore } from 'src/stores/user_highlight.js'
@@ -667,7 +668,7 @@ export const useUsersStore = defineStore('users', {
const startPolling = () => {
// Start getting fresh posts.
- dispatch('startFetchingTimeline', { timeline: 'friends' })
+ useTimelinesStore().startFetchingTimeline('friends')
// Start fetching notifications
dispatch('startFetchingNotifications')