TIMELINES STORE YEAH BABY
This commit is contained in:
parent
c723977d90
commit
cb25b392cf
21 changed files with 426 additions and 734 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
233
src/stores/timelines.js
Normal file
233
src/stores/timelines.js
Normal 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
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue