forked from AkkomaGang/akkoma-fe
d5d464a289
the list name is missing; this is because the API request to get it is different to the API request for receiving the list timeline, and is not yet implemented.
274 lines
10 KiB
JavaScript
274 lines
10 KiB
JavaScript
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
|
import { WSConnectionStatus } from '../services/api/api.service.js'
|
|
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
|
|
import { Socket } from 'phoenix'
|
|
|
|
const retryTimeout = (multiplier) => 1000 * multiplier
|
|
|
|
const api = {
|
|
state: {
|
|
retryMultiplier: 1,
|
|
backendInteractor: backendInteractorService(),
|
|
fetchers: {},
|
|
socket: null,
|
|
mastoUserSocket: null,
|
|
mastoUserSocketStatus: null,
|
|
followRequests: []
|
|
},
|
|
mutations: {
|
|
setBackendInteractor (state, backendInteractor) {
|
|
state.backendInteractor = backendInteractor
|
|
},
|
|
addFetcher (state, { fetcherName, fetcher }) {
|
|
state.fetchers[fetcherName] = fetcher
|
|
},
|
|
removeFetcher (state, { fetcherName, fetcher }) {
|
|
state.fetchers[fetcherName].stop()
|
|
delete state.fetchers[fetcherName]
|
|
},
|
|
setWsToken (state, token) {
|
|
state.wsToken = token
|
|
},
|
|
setSocket (state, socket) {
|
|
state.socket = socket
|
|
},
|
|
setFollowRequests (state, value) {
|
|
state.followRequests = value
|
|
},
|
|
setMastoUserSocketStatus (state, value) {
|
|
state.mastoUserSocketStatus = value
|
|
},
|
|
incrementRetryMultiplier (state) {
|
|
state.retryMultiplier = Math.max(++state.retryMultiplier, 3)
|
|
},
|
|
resetRetryMultiplier (state) {
|
|
state.retryMultiplier = 1
|
|
}
|
|
},
|
|
actions: {
|
|
/**
|
|
* Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets
|
|
*
|
|
* @param {Boolean} [initial] - whether this enabling happened at boot time or not
|
|
*/
|
|
enableMastoSockets (store, initial) {
|
|
const { state, dispatch, commit } = store
|
|
// Do not initialize unless nonexistent or closed
|
|
if (
|
|
state.mastoUserSocket &&
|
|
![
|
|
WebSocket.CLOSED,
|
|
WebSocket.CLOSING
|
|
].includes(state.mastoUserSocket.getState())
|
|
) {
|
|
return
|
|
}
|
|
if (initial) {
|
|
commit('setMastoUserSocketStatus', WSConnectionStatus.STARTING_INITIAL)
|
|
} else {
|
|
commit('setMastoUserSocketStatus', WSConnectionStatus.STARTING)
|
|
}
|
|
return dispatch('startMastoUserSocket')
|
|
},
|
|
disableMastoSockets (store) {
|
|
const { state, dispatch, commit } = store
|
|
if (!state.mastoUserSocket) return
|
|
commit('setMastoUserSocketStatus', WSConnectionStatus.DISABLED)
|
|
return dispatch('stopMastoUserSocket')
|
|
},
|
|
|
|
// MastoAPI 'User' sockets
|
|
startMastoUserSocket (store) {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
const { state, commit, dispatch, rootState } = store
|
|
const timelineData = rootState.statuses.timelines.friends
|
|
state.mastoUserSocket = state.backendInteractor.startUserSocket({ store })
|
|
state.mastoUserSocket.addEventListener(
|
|
'message',
|
|
({ detail: message }) => {
|
|
if (!message) return // pings
|
|
if (message.event === 'notification') {
|
|
dispatch('addNewNotifications', {
|
|
notifications: [message.notification],
|
|
older: false
|
|
})
|
|
} else if (message.event === 'update') {
|
|
dispatch('addNewStatuses', {
|
|
statuses: [message.status],
|
|
userId: false,
|
|
showImmediately: timelineData.visibleStatuses.length === 0,
|
|
timeline: 'friends'
|
|
})
|
|
} else if (message.event === 'delete') {
|
|
dispatch('deleteStatusById', message.id)
|
|
} else if (message.event === 'pleroma:chat_update') {
|
|
// The setTimeout wrapper is a temporary band-aid to avoid duplicates for the user's own messages when doing optimistic sending.
|
|
// The cause of the duplicates is the WS event arriving earlier than the HTTP response.
|
|
// This setTimeout wrapper can be removed once the commit `8e41baff` is in the stable Pleroma release.
|
|
// (`8e41baff` adds the idempotency key to the chat message entity, which PleromaFE uses when it's available, and it makes this artificial delay unnecessary).
|
|
setTimeout(() => {
|
|
dispatch('addChatMessages', {
|
|
chatId: message.chatUpdate.id,
|
|
messages: [message.chatUpdate.lastMessage]
|
|
})
|
|
dispatch('updateChat', { chat: message.chatUpdate })
|
|
maybeShowChatNotification(store, message.chatUpdate)
|
|
}, 100)
|
|
}
|
|
}
|
|
)
|
|
state.mastoUserSocket.addEventListener('open', () => {
|
|
// Do not show notification when we just opened up the page
|
|
if (state.mastoUserSocketStatus !== WSConnectionStatus.STARTING_INITIAL) {
|
|
dispatch('pushGlobalNotice', {
|
|
level: 'success',
|
|
messageKey: 'timeline.socket_reconnected',
|
|
timeout: 5000
|
|
})
|
|
}
|
|
// Stop polling if we were errored or disabled
|
|
if (new Set([
|
|
WSConnectionStatus.ERROR,
|
|
WSConnectionStatus.DISABLED
|
|
]).has(state.mastoUserSocketStatus)) {
|
|
dispatch('stopFetchingTimeline', { timeline: 'friends' })
|
|
dispatch('stopFetchingNotifications')
|
|
dispatch('stopFetchingChats')
|
|
}
|
|
commit('resetRetryMultiplier')
|
|
commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED)
|
|
})
|
|
state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
|
|
console.error('Error in MastoAPI websocket:', error)
|
|
// TODO is this needed?
|
|
dispatch('clearOpenedChats')
|
|
})
|
|
state.mastoUserSocket.addEventListener('close', ({ 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`)
|
|
commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED)
|
|
} else {
|
|
console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
|
|
setTimeout(() => {
|
|
dispatch('startMastoUserSocket')
|
|
}, retryTimeout(state.retryMultiplier))
|
|
commit('incrementRetryMultiplier')
|
|
if (state.mastoUserSocketStatus !== WSConnectionStatus.ERROR) {
|
|
dispatch('startFetchingTimeline', { timeline: 'friends' })
|
|
dispatch('startFetchingNotifications')
|
|
dispatch('startFetchingChats')
|
|
dispatch('pushGlobalNotice', {
|
|
level: 'error',
|
|
messageKey: 'timeline.socket_broke',
|
|
messageArgs: [code],
|
|
timeout: 5000
|
|
})
|
|
}
|
|
commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR)
|
|
}
|
|
dispatch('clearOpenedChats')
|
|
})
|
|
resolve()
|
|
} catch (e) {
|
|
reject(e)
|
|
}
|
|
})
|
|
},
|
|
stopMastoUserSocket ({ state, dispatch }) {
|
|
dispatch('startFetchingTimeline', { timeline: 'friends' })
|
|
dispatch('startFetchingNotifications')
|
|
dispatch('startFetchingChats')
|
|
state.mastoUserSocket.close()
|
|
},
|
|
|
|
// Timelines
|
|
startFetchingTimeline (store, {
|
|
timeline = 'friends',
|
|
tag = false,
|
|
userId = false,
|
|
listId = false
|
|
}) {
|
|
if (store.state.fetchers[timeline]) return
|
|
|
|
const fetcher = store.state.backendInteractor.startFetchingTimeline({
|
|
timeline, store, userId, listId, tag
|
|
})
|
|
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 }) {
|
|
store.state.backendInteractor.fetchTimeline({
|
|
store,
|
|
timeline,
|
|
...rest
|
|
})
|
|
},
|
|
|
|
// Notifications
|
|
startFetchingNotifications (store) {
|
|
if (store.state.fetchers.notifications) return
|
|
const fetcher = store.state.backendInteractor.startFetchingNotifications({ store })
|
|
store.commit('addFetcher', { fetcherName: 'notifications', fetcher })
|
|
},
|
|
stopFetchingNotifications (store) {
|
|
const fetcher = store.state.fetchers.notifications
|
|
if (!fetcher) return
|
|
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
|
|
},
|
|
fetchNotifications (store, { ...rest }) {
|
|
store.state.backendInteractor.fetchNotifications({
|
|
store,
|
|
...rest
|
|
})
|
|
},
|
|
|
|
// Follow requests
|
|
startFetchingFollowRequests (store) {
|
|
if (store.state.fetchers['followRequests']) return
|
|
const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
|
|
|
|
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
|
|
},
|
|
stopFetchingFollowRequests (store) {
|
|
const fetcher = store.state.fetchers.followRequests
|
|
if (!fetcher) return
|
|
store.commit('removeFetcher', { fetcherName: 'followRequests', fetcher })
|
|
},
|
|
removeFollowRequest (store, request) {
|
|
let requests = store.state.followRequests.filter((it) => it !== request)
|
|
store.commit('setFollowRequests', requests)
|
|
},
|
|
|
|
// Pleroma websocket
|
|
setWsToken (store, token) {
|
|
store.commit('setWsToken', token)
|
|
},
|
|
initializeSocket ({ dispatch, commit, state, rootState }) {
|
|
// Set up websocket connection
|
|
const token = state.wsToken
|
|
if (rootState.instance.shoutAvailable && typeof token !== 'undefined' && state.socket === null) {
|
|
const socket = new Socket('/socket', { params: { token } })
|
|
socket.connect()
|
|
|
|
commit('setSocket', socket)
|
|
dispatch('initializeShout', socket)
|
|
}
|
|
},
|
|
disconnectFromSocket ({ commit, state }) {
|
|
state.socket && state.socket.disconnect()
|
|
commit('setSocket', null)
|
|
}
|
|
}
|
|
}
|
|
|
|
export default api
|