initial streaming re-support
This commit is contained in:
parent
4de092808d
commit
652a9fabb7
11 changed files with 391 additions and 281 deletions
|
|
@ -77,7 +77,9 @@ export const useNotificationsStore = defineStore('notifications', {
|
|||
|
||||
useUsersStore().addNewUsers({
|
||||
timestamp,
|
||||
data: validNotifications.map((notification) => notification.from_profile),
|
||||
data: validNotifications.map(
|
||||
(notification) => notification.from_profile,
|
||||
),
|
||||
})
|
||||
|
||||
const statusNotifications = validNotifications.filter(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.j
|
|||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { useUsersStore } from 'src/stores/users.js'
|
||||
import { useStreamingStore } from 'src/stores/streaming.js'
|
||||
|
||||
import {
|
||||
fetchEmojiReactions,
|
||||
|
|
@ -78,7 +79,30 @@ const getLatestScrobble = (user) => {
|
|||
export const useStatusesStore = defineStore('statuses', {
|
||||
state: defaultState,
|
||||
actions: {
|
||||
addNewStatuses({ statuses, user = {}, userId, timestamp }) {
|
||||
// Init
|
||||
attachSocket() {
|
||||
const et = new EventTarget()
|
||||
const handleStatusMessage = ({ data, timestamp }) => {
|
||||
console.log('STATUSES', data)
|
||||
this.addNewStatuses({ statuses: [data.status], timestamp })
|
||||
}
|
||||
|
||||
et.addEventListener('update', ({ detail: message }) => {
|
||||
handleStatusMessage(message)
|
||||
})
|
||||
|
||||
et.addEventListener('status.update', ({ detail: message }) => {
|
||||
handleStatusMessage(message)
|
||||
})
|
||||
|
||||
et.addEventListener('delete', ({ detail: message }) => {
|
||||
console.log('DELETE', message)
|
||||
this.deleteStatus(message.data)
|
||||
})
|
||||
|
||||
useStreamingStore().addSubscriber({ et })
|
||||
},
|
||||
addNewStatuses({ statuses, timestamp }) {
|
||||
// Sanity check
|
||||
if (!Array.isArray(statuses)) {
|
||||
throw new TypeError("Statuses aren't an array!")
|
||||
|
|
@ -132,7 +156,7 @@ export const useStatusesStore = defineStore('statuses', {
|
|||
|
||||
if (status) {
|
||||
// This is our favorite, so the relevant bit.
|
||||
if (favorite.user.id === user.id) {
|
||||
if (favorite.user.id === useUsersStore().currentUser?.id) {
|
||||
status.favorited = true
|
||||
} else {
|
||||
status.fave_num += 1
|
||||
|
|
|
|||
189
src/stores/streaming.js
Normal file
189
src/stores/streaming.js
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
import { defineStore } from 'pinia'
|
||||
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
|
||||
import {
|
||||
getMastodonSocketURI,
|
||||
ProcessedWS,
|
||||
WSConnectionStatus,
|
||||
} from 'src/api/websocket.js'
|
||||
|
||||
const ARGUMENT_MAP = {
|
||||
tag: 'tag',
|
||||
list: 'list',
|
||||
}
|
||||
|
||||
export const TIMELINE_STREAM_MAP = {
|
||||
friends: 'user',
|
||||
public: 'public',
|
||||
tag: 'hashtag',
|
||||
list: 'list',
|
||||
dms: 'direct',
|
||||
}
|
||||
|
||||
const retryTimeout = (multiplier) => 1000 * multiplier
|
||||
|
||||
export const useStreamingStore = defineStore('streaming', {
|
||||
state: () => ({
|
||||
socket: null,
|
||||
error: null,
|
||||
state: null,
|
||||
retryMultiplier: 1,
|
||||
subscribers: new Set(),
|
||||
subscriptions: new Map(),
|
||||
globalSubscriptions: new Set(),
|
||||
}),
|
||||
actions: {
|
||||
addSubscriber(subscriber) {
|
||||
const { stream, et } = subscriber
|
||||
|
||||
if (stream) {
|
||||
if (!this.subscriptions.has(stream.name)) {
|
||||
this.subscriptions.set(stream.name, new Map())
|
||||
}
|
||||
|
||||
const streamSubs = this.subscriptions.get(stream.name)
|
||||
|
||||
if (streamSubs.has(stream.argument)) {
|
||||
throw new Error('Subscription already exists!')
|
||||
}
|
||||
|
||||
streamSubs.set(stream.argument, subscriber)
|
||||
} else {
|
||||
this.globalSubscriptions.add(subscriber)
|
||||
}
|
||||
|
||||
this.subscribers.add(subscriber)
|
||||
if (this.state === WSConnectionStatus.JOINED) {
|
||||
this.socket.subscribe(...this.getSubArgs(stream))
|
||||
subscriber.et.dispatchEvent(new CustomEvent('open'))
|
||||
}
|
||||
},
|
||||
removeSubscriber(subscriber) {
|
||||
const { stream, et } = subscriber
|
||||
|
||||
this.subscribers.delete(subscriber)
|
||||
this.subscriptions.get(stream.name).delete(stream.argument)
|
||||
|
||||
if (this.state === WSConnectionStatus.JOINED) {
|
||||
this.socket.unsubscribe(...this.getSubArgs(stream))
|
||||
}
|
||||
},
|
||||
initSocket(initial) {
|
||||
this.state = initial
|
||||
? WSConnectionStatus.STARTING_INITIAL
|
||||
: WSConnectionStatus.STARTING
|
||||
|
||||
const credentials = useOAuthStore().token
|
||||
const url = getMastodonSocketURI({ credentials })
|
||||
|
||||
this.socket = ProcessedWS({
|
||||
url,
|
||||
id: 'Unified',
|
||||
credentials,
|
||||
})
|
||||
|
||||
this.socket.addEventListener('pleroma:authenticated', this.onAuth)
|
||||
this.socket.addEventListener('open', this.onOpen)
|
||||
this.socket.addEventListener('close', this.onClose)
|
||||
this.socket.addEventListener('message', this.onMessage)
|
||||
this.socket.addEventListener('error', this.onError)
|
||||
},
|
||||
stopSocket() {
|
||||
this.socket.close()
|
||||
},
|
||||
|
||||
getSubArgs(stream) {
|
||||
const argumentKey = ARGUMENT_MAP[stream.name]
|
||||
const args = argumentKey
|
||||
? {
|
||||
[argumentKey]: stream.argument,
|
||||
}
|
||||
: null
|
||||
|
||||
return [stream.name, args]
|
||||
},
|
||||
onAuth() {
|
||||
this.subscribers.forEach(({ stream, et }) => {
|
||||
et.dispatchEvent(new CustomEvent('authenticated'))
|
||||
|
||||
if (stream) {
|
||||
this.socket.subscribe(...this.getSubArgs(stream))
|
||||
}
|
||||
})
|
||||
this.state = WSConnectionStatus.JOINED
|
||||
},
|
||||
onOpen() {
|
||||
this.subscribers.forEach(({ stream, et }) => {
|
||||
et.dispatchEvent(new CustomEvent('open'))
|
||||
})
|
||||
},
|
||||
onMessage({ detail: message }) {
|
||||
if (!message) return // pings
|
||||
const timestamp = Date.now()
|
||||
const { event: eventName, stream: eventStream, ...data } = message
|
||||
const [streamName, streamArgument] = eventStream ?? []
|
||||
|
||||
const subscriber = this.subscriptions.get(streamName)?.get(streamArgument)
|
||||
|
||||
const totalSubs = [
|
||||
...this.globalSubscriptions.values(),
|
||||
subscriber
|
||||
].filter(Boolean)
|
||||
|
||||
totalSubs.forEach(({ stream, et }) => {
|
||||
et.dispatchEvent(new CustomEvent(
|
||||
eventName,
|
||||
{
|
||||
detail: { streamName, streamArgument, data, timestamp },
|
||||
}
|
||||
))
|
||||
})
|
||||
|
||||
console.log('WS', message)
|
||||
},
|
||||
onError({ detail: error }) {
|
||||
this.subscribers.forEach(({ stream, et }) => {
|
||||
et.dispatchEvent(new CustomEvent('error', error))
|
||||
})
|
||||
console.error('Error in MastoAPI websocket:', error)
|
||||
},
|
||||
onClose({ detail: closeEvent }) {
|
||||
const ignoreCodes = new Set([
|
||||
1000, // Normal (intended) closure
|
||||
1001, // Going away
|
||||
])
|
||||
const { code } = closeEvent
|
||||
|
||||
if (ignoreCodes.has(code)) {
|
||||
console.debug(
|
||||
`Not restarting socket becasue of closure code ${code} is in ignore list`,
|
||||
)
|
||||
|
||||
this.state = WSConnectionStatus.CLOSED
|
||||
|
||||
this.subscribers.forEach(({ et }) => {
|
||||
et.dispatchEvent(new CustomEvent('close', closeEvent))
|
||||
})
|
||||
} else {
|
||||
console.warn(
|
||||
`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`,
|
||||
)
|
||||
|
||||
setTimeout(() => {
|
||||
this.initSocket()
|
||||
}, retryTimeout(this.retryMultiplier))
|
||||
|
||||
this.retryMultiplier += 1
|
||||
|
||||
if (this.state !== WSConnectionStatus.ERROR) {
|
||||
this.subscribers.forEach(({ et }) => {
|
||||
et.dispatchEvent(new CustomEvent('close', closeEvent))
|
||||
})
|
||||
}
|
||||
|
||||
this.state = WSConnectionStatus.ERROR
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -4,6 +4,8 @@ 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 { useStatusesStore } from 'src/stores/statuses.js'
|
||||
import { useStreamingStore, TIMELINE_STREAM_MAP } from 'src/stores/streaming.js'
|
||||
|
||||
import timelineFetcher from 'src/services/timeline_fetcher/timeline_fetcher.service.js'
|
||||
|
||||
|
|
@ -17,6 +19,7 @@ const emptyTl = (name, argument = null) => {
|
|||
minId: '',
|
||||
minVisibleId: '',
|
||||
loading: false,
|
||||
streaming: false,
|
||||
flushMarker: 0,
|
||||
fetcher: null,
|
||||
}
|
||||
|
|
@ -27,6 +30,10 @@ const emptyTl = (name, argument = null) => {
|
|||
result[property] = argument
|
||||
}
|
||||
|
||||
if (name === 'dms' || name === 'friends') {
|
||||
result.persistent = true
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
@ -41,24 +48,28 @@ export const ARGUMENT_MAP = {
|
|||
media: 'userId',
|
||||
}
|
||||
|
||||
const TIMELINES = new Set([
|
||||
'mentions',
|
||||
'public',
|
||||
'user',
|
||||
'userPinned',
|
||||
'media',
|
||||
'favorites',
|
||||
'publicAndExternal',
|
||||
'friends',
|
||||
'tag',
|
||||
'dms',
|
||||
'bookmarks',
|
||||
'list',
|
||||
'bubble',
|
||||
'quotes',
|
||||
'search',
|
||||
])
|
||||
|
||||
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)]))
|
||||
return Object.fromEntries(
|
||||
[...TIMELINES].map((name) => [name, emptyTl(name)]),
|
||||
)
|
||||
}
|
||||
|
||||
//const CUSTOM_SORT = new Set(['bookmarks', 'favorites'])
|
||||
|
|
@ -123,7 +134,10 @@ export const useTimelinesStore = defineStore('timelines', {
|
|||
) {
|
||||
// Add the mention to the mentions timeline
|
||||
if (timeline !== this.mentions) {
|
||||
this.addStatusesToTimeline('mentions', null, { statuses, nested: true })
|
||||
this.addStatusesToTimeline('mentions', null, {
|
||||
statuses,
|
||||
nested: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,10 +149,17 @@ export const useTimelinesStore = defineStore('timelines', {
|
|||
})
|
||||
},
|
||||
|
||||
// Fetchers
|
||||
startFetchingTimeline(timelineName, argument) {
|
||||
activatePersistents() {
|
||||
TIMELINES.forEach(name => {
|
||||
if (this[name].persistent) {
|
||||
this.activate(name, undefined, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
activate(timelineName, argument, persistent) {
|
||||
const timeline = this[timelineName]
|
||||
if (timeline.fetcher) return
|
||||
if (timeline.persistent && !persistent) return
|
||||
|
||||
if (
|
||||
timelineName === 'favourites' &&
|
||||
|
|
@ -153,12 +174,64 @@ export const useTimelinesStore = defineStore('timelines', {
|
|||
useOAuthStore().token,
|
||||
)
|
||||
|
||||
this.startFetchingTimeline(timelineName, argument)
|
||||
|
||||
const streamName = TIMELINE_STREAM_MAP[timelineName]
|
||||
|
||||
if (streamName) {
|
||||
const et = new EventTarget()
|
||||
et.addEventListener('open', () => this.onStreamConnect(timelineName, argument))
|
||||
et.addEventListener('close', () => this.onStreamDisconnect(timelineName, argument))
|
||||
et.addEventListener('update', ({ detail: message }) => this.onStreamMessage(timelineName, argument, message))
|
||||
|
||||
timeline.socket = {
|
||||
stream: {
|
||||
name: streamName,
|
||||
argument,
|
||||
},
|
||||
et,
|
||||
}
|
||||
|
||||
useStreamingStore().addSubscriber(timeline.socket)
|
||||
}
|
||||
},
|
||||
deactivate(timelineName) {
|
||||
const timeline = this[timelineName]
|
||||
if (timeline.persistent) return
|
||||
this.clearTimeline(timelineName)
|
||||
},
|
||||
|
||||
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],
|
||||
})
|
||||
},
|
||||
|
||||
onStreamConnect(timeline) {
|
||||
console.log('STREAM OK', timeline)
|
||||
this[timeline].streaming = true
|
||||
this.stopFetchingTimeline(timeline)
|
||||
},
|
||||
|
||||
onStreamDisconnect(timeline, argument) {
|
||||
console.log('STREAM DED', timeline, argument)
|
||||
this[timeline].streaming = false
|
||||
this.startFetchingTimeline(timeline, argument)
|
||||
},
|
||||
|
||||
// Fetchers
|
||||
startFetchingTimeline(timelineName, argument) {
|
||||
console.log('START FETCHING', timelineName, argument)
|
||||
const timeline = this[timelineName]
|
||||
timeline.fetcher.startFetching()
|
||||
},
|
||||
stopFetchingTimeline(timelineName) {
|
||||
console.log('STOP FETCHING', timelineName)
|
||||
const timeline = this[timelineName]
|
||||
timeline.fetcher?.stopFetching()
|
||||
timeline.fetcher = null
|
||||
timeline.fetcher.stopFetching()
|
||||
},
|
||||
|
||||
// Queues & Timeline manip
|
||||
|
|
@ -198,10 +271,16 @@ export const useTimelinesStore = defineStore('timelines', {
|
|||
|
||||
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)
|
||||
clearTimeline(timeline) {
|
||||
console.log('CLEAR TIMELINE', timeline)
|
||||
const data = this[timeline]
|
||||
if (!data.streaming) {
|
||||
this.stopFetchingTimeline(timeline)
|
||||
}
|
||||
if (data.socket) {
|
||||
useStreamingStore().removeSubscriber(data.socket)
|
||||
}
|
||||
this[timeline] = emptyTl(timeline)
|
||||
},
|
||||
queueFlush(timeline, id) {
|
||||
this[timeline].flushMarker = id
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ 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 { useTimelinesStore } from 'src/stores/timelines.js'
|
||||
import { useStreamingStore } from 'src/stores/streaming.js'
|
||||
import { useUserHighlightStore } from 'src/stores/user_highlight.js'
|
||||
|
||||
import { revokeToken } from 'src/api/oauth.js'
|
||||
|
|
@ -391,9 +392,7 @@ export const useUsersStore = defineStore('users', {
|
|||
fetchUserRelationship({
|
||||
id,
|
||||
credentials: useOAuthStore().token,
|
||||
}).then((result) =>
|
||||
this.updateUserRelationships(result),
|
||||
)
|
||||
}).then((result) => this.updateUserRelationships(result))
|
||||
}
|
||||
},
|
||||
fetchUserInLists(id) {
|
||||
|
|
@ -431,7 +430,7 @@ export const useUsersStore = defineStore('users', {
|
|||
const predictedRelationship = this.relationships[id] || { id }
|
||||
this.updateUserRelationships({
|
||||
optimism: true,
|
||||
data: [predictedRelationship]
|
||||
data: [predictedRelationship],
|
||||
})
|
||||
this.addBlockId(id)
|
||||
|
||||
|
|
@ -494,7 +493,7 @@ export const useUsersStore = defineStore('users', {
|
|||
predictedRelationship.muting = true
|
||||
this.updateUserRelationships({
|
||||
optimism: true,
|
||||
data: [predictedRelationship]
|
||||
data: [predictedRelationship],
|
||||
})
|
||||
this.addMuteId(id)
|
||||
|
||||
|
|
@ -512,7 +511,7 @@ export const useUsersStore = defineStore('users', {
|
|||
predictedRelationship.muting = false
|
||||
this.updateUserRelationships({
|
||||
optimism: true,
|
||||
data: [predictedRelationship]
|
||||
data: [predictedRelationship],
|
||||
})
|
||||
|
||||
return unmuteUser({ id }).then(({ data: relationship }) =>
|
||||
|
|
@ -524,18 +523,14 @@ export const useUsersStore = defineStore('users', {
|
|||
id,
|
||||
reblogs: false,
|
||||
credentials: useOAuthStore().token,
|
||||
}).then(({ data: relationship }) =>
|
||||
this.updateUserRelationships(data),
|
||||
)
|
||||
}).then(({ data: relationship }) => this.updateUserRelationships(data))
|
||||
},
|
||||
showReblogs(id) {
|
||||
return followUser({
|
||||
id,
|
||||
reblogs: true,
|
||||
credentials: useOAuthStore().token,
|
||||
}).then(({ data: relationship }) =>
|
||||
this.updateUserRelationships(data),
|
||||
)
|
||||
}).then(({ data: relationship }) => this.updateUserRelationships(data))
|
||||
},
|
||||
muteUsers(data = []) {
|
||||
return Promise.all(data.map((d) => this.muteUser(d)))
|
||||
|
|
@ -600,18 +595,14 @@ export const useUsersStore = defineStore('users', {
|
|||
id,
|
||||
notify: true,
|
||||
credentials: useOAuthStore().token,
|
||||
}).then(({ data: relationship }) =>
|
||||
this.updateUserRelationships(data),
|
||||
)
|
||||
}).then(({ data: relationship }) => this.updateUserRelationships(data))
|
||||
},
|
||||
unsubscribeUser(id) {
|
||||
return followUser({
|
||||
id,
|
||||
notify: false,
|
||||
credentials: useOAuthStore().token,
|
||||
}).then(({ data: relationship }) =>
|
||||
this.updateUserRelationships(data),
|
||||
)
|
||||
}).then(({ data: relationship }) => this.updateUserRelationships(data))
|
||||
},
|
||||
registerPushNotifications() {
|
||||
const token = this.currentUser.credentials
|
||||
|
|
@ -729,55 +720,31 @@ export const useUsersStore = defineStore('users', {
|
|||
/**/
|
||||
|
||||
if (user.token) {
|
||||
// Shoutbox
|
||||
dispatch('setWsToken', user.token)
|
||||
|
||||
// Initialize the shout socket.
|
||||
dispatch('initializeSocket')
|
||||
}
|
||||
|
||||
const startPolling = () => {
|
||||
// Start getting fresh posts.
|
||||
useTimelinesStore().startFetchingTimeline('friends')
|
||||
// DMs and Home
|
||||
useTimelinesStore().activatePersistents()
|
||||
|
||||
// Start fetching notifications
|
||||
dispatch('startFetchingNotifications')
|
||||
|
||||
if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) {
|
||||
// Start fetching chats
|
||||
useChatsStore().startFetchingChats()
|
||||
}
|
||||
if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) {
|
||||
// Start fetching chats
|
||||
useChatsStore().startFetchingChats()
|
||||
}
|
||||
|
||||
useListsStore().startFetching()
|
||||
useBookmarkFoldersStore().startFetching()
|
||||
useStatusesStore().attachSocket()
|
||||
//useNotificationsStore().attachSocket()
|
||||
|
||||
if (user.locked) {
|
||||
dispatch('startFetchingFollowRequests')
|
||||
}
|
||||
|
||||
useStreamingStore().initSocket()
|
||||
if (useMergedConfigStore().mergedConfig.useStreamingApi) {
|
||||
dispatch('fetchTimeline', {
|
||||
timeline: 'friends',
|
||||
sinceId: null,
|
||||
})
|
||||
dispatch('fetchNotifications', { sinceId: null })
|
||||
dispatch('enableMastoSockets', true)
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed initializing MastoAPI Streaming socket',
|
||||
error,
|
||||
)
|
||||
})
|
||||
.then(() => {
|
||||
dispatch('fetchChats', { latest: true })
|
||||
setTimeout(
|
||||
() =>
|
||||
useNotificationsStore().setNotificationsSilence(false),
|
||||
10000,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
startPolling()
|
||||
useStreamingStore().initSocket()
|
||||
}
|
||||
|
||||
// Start fetching things that don't need to block the UI
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue