pleroma-fe/src/stores/timelines.js

338 lines
9.2 KiB
JavaScript
Raw Normal View History

2026-08-11 18:54:54 +03:00
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'
2026-08-13 01:12:48 +03:00
import { useStatusesStore } from 'src/stores/statuses.js'
2026-08-13 01:22:44 +03:00
import { TIMELINE_STREAM_MAP, useStreamingStore } from 'src/stores/streaming.js'
import { useUsersStore } from 'src/stores/users.js'
2026-08-11 18:54:54 +03:00
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,
2026-08-11 20:59:18 +03:00
maxId: '',
minId: '',
minVisibleId: '',
2026-08-11 18:54:54 +03:00
loading: false,
2026-08-13 01:12:48 +03:00
streaming: false,
2026-08-11 18:54:54 +03:00
flushMarker: 0,
fetcher: null,
}
2026-08-11 20:25:43 +03:00
const property = ARGUMENT_MAP[name]
2026-08-11 18:54:54 +03:00
if (property) {
result[property] = argument
}
2026-08-13 01:12:48 +03:00
if (name === 'dms' || name === 'friends') {
result.persistent = true
}
2026-08-11 18:54:54 +03:00
return result
}
export const ARGUMENT_MAP = {
tag: 'tag',
list: 'listId',
bookmarks: 'bookmarkFolderId',
quotes: 'statusId',
search: 'query',
2026-08-11 20:25:43 +03:00
user: 'userId',
userPinned: 'userId',
media: 'userId',
2026-08-11 18:54:54 +03:00
}
2026-08-13 01:12:48 +03:00
const TIMELINES = new Set([
'mentions',
'public',
'user',
'userPinned',
'media',
'favorites',
'publicAndExternal',
'friends',
'tag',
'dms',
'bookmarks',
'list',
'bubble',
'quotes',
'search',
])
2026-08-11 18:54:54 +03:00
export const defaultState = () => {
2026-08-13 01:22:44 +03:00
return Object.fromEntries([...TIMELINES].map((name) => [name, emptyTl(name)]))
2026-08-11 18:54:54 +03:00
}
//const CUSTOM_SORT = new Set(['bookmarks', 'favorites'])
export const useTimelinesStore = defineStore('timelines', {
state: defaultState,
actions: {
2026-08-13 16:45:44 +03:00
// (De)Initialization stuff
activate(timelineName, argument, persistent) {
const timeline = this[timelineName]
if (timeline.persistent && !persistent) return
if (
timelineName === 'favourites' &&
!useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable
) {
2026-08-14 19:41:28 +03:00
console.warn("Instance doesn't support public favorites timeline")
2026-08-13 16:45:44 +03:00
return
}
timeline.fetcher = timelineFetcher(
timeline,
argument,
useOAuthStore().token,
)
this.startFetchingTimeline(timelineName, argument, 'Timeline activated')
const streamName = TIMELINE_STREAM_MAP[timelineName]
if (streamName) {
const et = new EventTarget()
const openHandler = () => this.onStreamConnect(timelineName, argument)
const closeHandler = () =>
this.onStreamDisconnect(timelineName, argument)
const messageHandler =
() =>
({ detail: message }) =>
this.onStreamMessage(timelineName, argument, message)
2026-08-13 16:45:44 +03:00
et.addEventListener('open', openHandler)
et.addEventListener('close', closeHandler)
et.addEventListener('update', messageHandler)
timeline.socket = {
stream: {
name: streamName,
argument,
},
et,
handlers: {
openHandler,
closeHandler,
messageHandler,
},
2026-08-13 16:45:44 +03:00
}
useStreamingStore().addSubscriber(timeline.socket)
}
},
deactivate(timelineName, persistent) {
const timeline = this[timelineName]
if (timeline.persistent && !persistent) return
if (!timeline.streaming) {
this.stopFetchingTimeline(timelineName, 'Timeline deactivation')
}
if (timeline.socket) {
2026-08-13 16:45:44 +03:00
useStreamingStore().removeSubscriber(timeline.socket)
const { openHandler, closeHandler, messageHandler } =
timeline.socket.handlers
timeline.socket.et.removeEventListener('open', openHandler)
timeline.socket.et.removeEventListener('close', closeHandler)
timeline.socket.et.removeEventListener('message', messageHandler)
2026-08-13 16:45:44 +03:00
}
this[timelineName] = emptyTl(timelineName)
},
activatePersistents() {
TIMELINES.forEach((name) => {
if (this[name].persistent) {
this.activate(name, undefined, true)
}
})
},
deactivateAll() {
TIMELINES.forEach((name) => {
this.deactivate(name, true)
})
},
// Update stuff
2026-08-11 18:54:54 +03:00
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.
2026-08-11 20:25:43 +03:00
const property = ARGUMENT_MAP[name]
2026-08-11 18:54:54 +03:00
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) {
2026-08-13 01:12:48 +03:00
this.addStatusesToTimeline('mentions', null, {
2026-08-14 17:51:43 +03:00
statuses: [status],
2026-08-13 01:12:48 +03:00
nested: true,
})
2026-08-11 18:54:54 +03:00
}
}
if (status.visibility === 'direct') {
if (timeline !== this.dms) {
2026-08-14 19:41:28 +03:00
this.addStatusesToTimeline('dms', null, {
statuses: [status],
nested: true,
})
2026-08-11 18:54:54 +03:00
}
}
})
},
2026-08-13 01:12:48 +03:00
onStreamMessage(timeline, argument, event) {
// This relies on statuses store to process this event first
const status = useStatusesStore().allStatuses.get(event.data.status.id)
this.addStatusesToTimeline(timeline, argument, {
statuses: [status],
})
},
2026-08-13 16:45:44 +03:00
// Poll & Push
2026-08-13 01:12:48 +03:00
onStreamConnect(timeline) {
2026-08-13 16:45:44 +03:00
console.debug('[Timelines] Stream connected', timeline)
2026-08-13 01:12:48 +03:00
this[timeline].streaming = true
2026-08-13 16:45:44 +03:00
this.stopFetchingTimeline(timeline, 'Socket connected')
2026-08-13 01:12:48 +03:00
},
onStreamDisconnect(timeline, argument) {
2026-08-13 16:45:44 +03:00
console.debug('[Timelines] Stream disconnected', timeline, argument)
2026-08-13 01:12:48 +03:00
this[timeline].streaming = false
2026-08-13 16:45:44 +03:00
this.startFetchingTimeline(timeline, argument, 'Socket disconnected')
2026-08-13 01:12:48 +03:00
},
2026-08-13 16:45:44 +03:00
startFetchingTimeline(timelineName, argument, reason) {
console.debug(
'[Timelines] Starting fetching timeline',
timelineName,
argument,
'Reason:',
reason,
)
2026-08-13 01:12:48 +03:00
const timeline = this[timelineName]
2026-08-11 18:54:54 +03:00
timeline.fetcher.startFetching()
},
2026-08-13 16:45:44 +03:00
stopFetchingTimeline(timelineName, reason) {
console.debug(
'[Timelines] Stopped fetching timeline',
timelineName,
'Reason:',
reason,
)
2026-08-11 18:54:54 +03:00
const timeline = this[timelineName]
2026-08-13 01:12:48 +03:00
timeline.fetcher.stopFetching()
2026-08-11 18:54:54 +03:00
},
// 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) ?? ''
2026-08-11 20:59:18 +03:00
const newer = maxNew > timeline.maxId
const older = minNew < timeline.minId
2026-08-11 18:54:54 +03:00
2026-08-11 20:59:18 +03:00
if (newer || timeline.maxId === '') {
2026-08-11 18:54:54 +03:00
timeline.maxId = maxNew
}
2026-08-11 20:59:18 +03:00
if (older || timeline.minId === '') {
2026-08-11 18:54:54 +03:00
timeline.minId = minNew
}
},
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()])
},
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
},
},
})