pleroma-fe/src/stores/timelines.js

321 lines
8.5 KiB
JavaScript
Raw Normal View History

2026-08-19 19:19:59 +03:00
import { first, last } from 'lodash'
2026-08-11 18:54:54 +03:00
import { defineStore } from 'pinia'
2026-08-19 19:19:59 +03:00
import timelineFetcher from 'src/stores/fetchers/timeline_fetcher.js'
2026-08-11 18:54:54 +03:00
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useOAuthStore } from 'src/stores/oauth.js'
2026-08-13 01:22:44 +03:00
import { TIMELINE_STREAM_MAP, useStreamingStore } from 'src/stores/streaming.js'
2026-08-11 18:54:54 +03:00
const emptyTl = (name, argument = null) => {
const result = {
name,
2026-08-18 21:35:06 +03:00
order: [],
statusIds: new Set(),
visibleStatusIds: new Set(),
2026-08-11 18:54:54 +03:00
newStatusCount: 0,
2026-08-11 20:59:18 +03:00
maxId: '',
minId: '',
2026-08-13 01:12:48 +03:00
streaming: false,
2026-08-24 15:01:59 +03:00
reloadNeeded: false,
2026-08-11 18:54:54 +03:00
fetcher: null,
2026-08-18 21:35:06 +03:00
socket: null,
2026-08-11 18:54:54 +03:00
}
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
}
2026-08-18 21:35:06 +03:00
const property = ARGUMENT_MAP[timelineName]
if (property) {
timeline[property] = argument
}
2026-08-13 16:45:44 +03:00
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)
},
2026-08-21 00:12:14 +03:00
clearTimeline(timelineName) {
const timeline = this[timelineName]
timeline.order = []
timeline.statusIds = new Set()
timeline.visibleStatusIds = new Set()
timeline.newStatusCount = 0
timeline.maxId = ''
timeline.minId = ''
2026-08-24 15:01:59 +03:00
timeline.reloadNeeded = false
2026-08-21 00:12:14 +03:00
},
2026-08-13 16:45:44 +03:00
activatePersistents() {
TIMELINES.forEach((name) => {
if (this[name].persistent) {
this.activate(name, undefined, true)
}
})
},
deactivateAll() {
TIMELINES.forEach((name) => {
2026-08-18 21:35:06 +03:00
try {
this.deactivate(name, true)
} catch (e) {
2026-08-19 19:19:59 +03:00
console.error(`Failed to deactivate timeline ${name}:`, e)
2026-08-18 21:35:06 +03:00
}
2026-08-13 16:45:44 +03:00
})
},
// Update stuff
2026-08-11 18:54:54 +03:00
addStatusesToTimeline(
timelineName,
argument,
{
statuses,
showImmediately = false,
noIdUpdate = false,
pagination = {},
2026-08-19 19:19:59 +03:00
older = false,
2026-08-11 18:54:54 +03:00
},
) {
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-18 21:35:06 +03:00
const property = ARGUMENT_MAP[timelineName]
2026-08-11 18:54:54 +03:00
if (property && timeline[property] !== argument) {
return
}
if (!noIdUpdate) {
2026-08-19 19:19:59 +03:00
this.updateTimelineExtremes(timeline, pagination)
2026-08-11 18:54:54 +03:00
}
2026-08-18 21:35:06 +03:00
const filtered = statuses.filter((id) => !timeline.statusIds.has(id))
if (older) {
timeline.order.push(...filtered)
} else {
timeline.order.unshift(...filtered)
}
statuses.forEach((statusId) => {
const isNew = !timeline.statusIds.has(statusId)
timeline.statusIds.add(statusId)
2026-08-11 18:54:54 +03:00
if (isNew) {
if (showImmediately) {
// Add it directly to the visibleStatuses, don't change
// newStatusCount
2026-08-18 21:35:06 +03:00
timeline.visibleStatusIds.add(statusId)
2026-08-11 18:54:54 +03:00
} else {
// Just change newStatuscount
timeline.newStatusCount += 1
}
}
})
},
2026-08-13 01:12:48 +03:00
onStreamMessage(timeline, argument, event) {
this.addStatusesToTimeline(timeline, argument, {
2026-08-18 21:35:06 +03:00
statuses: [event.data.status.id],
2026-08-13 01:12:48 +03:00
})
},
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
2026-08-18 21:35:06 +03:00
updateTimelineExtremes(timeline, pagination = {}) {
2026-08-11 18:54:54 +03:00
// Can't use Math.min/max because it doesn't work with string (duh)
2026-08-18 21:35:06 +03:00
const minNew = pagination.maxId ?? last(timeline.order) ?? ''
const maxNew = pagination.minId ?? first(timeline.order) ?? ''
2026-08-11 18:54:54 +03:00
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
}
2026-08-18 21:35:06 +03:00
this.syncOrder(timeline)
2026-08-11 18:54:54 +03:00
},
showNewStatuses(timelineName) {
const timeline = this[timelineName]
timeline.newStatusCount = 0
2026-08-18 21:35:06 +03:00
timeline.visibleStatusIds = new Set([...timeline.statusIds])
},
syncOrder(timeline) {
timeline.order = timeline.order.filter((id) => timeline.statusIds.has(id))
2026-08-11 18:54:54 +03:00
},
2026-08-24 15:01:59 +03:00
requireReload(timeline, id) {
this[timeline].reloadNeeded = true
2026-08-11 18:54:54 +03:00
},
2026-08-24 15:01:59 +03:00
requireReloadAll() {
2026-08-11 18:54:54 +03:00
Object.keys(this).forEach((timeline) => {
2026-08-24 15:01:59 +03:00
this[timeline].reloadNeeded = true
2026-08-11 18:54:54 +03:00
})
},
// Misc
2026-08-18 21:35:06 +03:00
wipeStatuses(ids) {
2026-08-18 03:20:09 +03:00
TIMELINES.forEach((timelineName) => {
2026-08-18 21:35:06 +03:00
const timeline = this[timelineName]
ids.forEach((id) => {
timeline.statusIds.delete(id)
timeline.visibleStatusIds.delete(id)
})
this.syncOrder(timeline)
2026-08-18 03:20:09 +03:00
})
2026-08-11 18:54:54 +03:00
},
},
})