forked from AkkomaGang/akkoma-fe
Merge branch 'websocket-fixes' into 'develop'
Various websocket fixes See merge request pleroma/pleroma-fe!1326
This commit is contained in:
commit
a00212a3bb
13 changed files with 168 additions and 35 deletions
|
@ -37,6 +37,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
### Fixed
|
### Fixed
|
||||||
- Follows/Followers tabs on user profiles now display the content properly.
|
- Follows/Followers tabs on user profiles now display the content properly.
|
||||||
- Handle punycode in screen names
|
- Handle punycode in screen names
|
||||||
|
- Fixed local dev mode having non-functional websockets in some cases
|
||||||
|
- Show notices for websocket events (errors, abnormal closures, reconnections)
|
||||||
|
- Fix not being able to re-enable websocket until page refresh
|
||||||
|
- Fix annoying issue where timeline might have few posts when streaming is enabled
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Don't filter own posts when they hit your wordfilter
|
- Don't filter own posts when they hit your wordfilter
|
||||||
|
|
|
@ -3,6 +3,11 @@ const path = require('path')
|
||||||
let settings = {}
|
let settings = {}
|
||||||
try {
|
try {
|
||||||
settings = require('./local.json')
|
settings = require('./local.json')
|
||||||
|
if (settings.target && settings.target.endsWith('/')) {
|
||||||
|
// replacing trailing slash since it can conflict with some apis
|
||||||
|
// and that's how actual BE reports its url
|
||||||
|
settings.target = settings.target.replace(/\/$/, '')
|
||||||
|
}
|
||||||
console.log('Using local dev server settings (/config/local.json):')
|
console.log('Using local dev server settings (/config/local.json):')
|
||||||
console.log(JSON.stringify(settings, null, 2))
|
console.log(JSON.stringify(settings, null, 2))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -706,6 +706,15 @@ nav {
|
||||||
color: var(--alertWarningPanelText, $fallback--text);
|
color: var(--alertWarningPanelText, $fallback--text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background-color: var(--alertSuccess, $fallback--alertWarning);
|
||||||
|
color: var(--alertSuccessText, $fallback--text);
|
||||||
|
|
||||||
|
.panel-heading & {
|
||||||
|
color: var(--alertSuccessPanelText, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.faint {
|
.faint {
|
||||||
|
|
|
@ -71,6 +71,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.global-success {
|
||||||
|
background-color: var(--alertPopupSuccess, $fallback--cGreen);
|
||||||
|
color: var(--alertPopupSuccessText, $fallback--text);
|
||||||
|
.svg-inline--fa {
|
||||||
|
color: var(--alertPopupSuccessText, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.global-info {
|
.global-info {
|
||||||
background-color: var(--alertPopupNeutral, $fallback--fg);
|
background-color: var(--alertPopupNeutral, $fallback--fg);
|
||||||
color: var(--alertPopupNeutralText, $fallback--text);
|
color: var(--alertPopupNeutralText, $fallback--text);
|
||||||
|
|
|
@ -35,11 +35,6 @@ const Notifications = {
|
||||||
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
|
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
|
||||||
const store = this.$store
|
|
||||||
const credentials = store.state.users.currentUser.credentials
|
|
||||||
notificationsFetcher.fetchAndUpdate({ store, credentials })
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
mainClass () {
|
mainClass () {
|
||||||
return this.minimalMode ? '' : 'panel panel-default'
|
return this.minimalMode ? '' : 'panel panel-default'
|
||||||
|
|
|
@ -663,7 +663,9 @@
|
||||||
"reload": "Reload",
|
"reload": "Reload",
|
||||||
"up_to_date": "Up-to-date",
|
"up_to_date": "Up-to-date",
|
||||||
"no_more_statuses": "No more statuses",
|
"no_more_statuses": "No more statuses",
|
||||||
"no_statuses": "No statuses"
|
"no_statuses": "No statuses",
|
||||||
|
"socket_reconnected": "Realtime connection established",
|
||||||
|
"socket_broke": "Realtime connection lost: CloseEvent code {0}"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"favorites": "Favorites",
|
"favorites": "Favorites",
|
||||||
|
|
|
@ -3,8 +3,11 @@ import { WSConnectionStatus } from '../services/api/api.service.js'
|
||||||
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
|
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
|
||||||
import { Socket } from 'phoenix'
|
import { Socket } from 'phoenix'
|
||||||
|
|
||||||
|
const retryTimeout = (multiplier) => 1000 * multiplier
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
state: {
|
state: {
|
||||||
|
retryMultiplier: 1,
|
||||||
backendInteractor: backendInteractorService(),
|
backendInteractor: backendInteractorService(),
|
||||||
fetchers: {},
|
fetchers: {},
|
||||||
socket: null,
|
socket: null,
|
||||||
|
@ -34,18 +37,43 @@ const api = {
|
||||||
},
|
},
|
||||||
setMastoUserSocketStatus (state, value) {
|
setMastoUserSocketStatus (state, value) {
|
||||||
state.mastoUserSocketStatus = value
|
state.mastoUserSocketStatus = value
|
||||||
|
},
|
||||||
|
incrementRetryMultiplier (state) {
|
||||||
|
state.retryMultiplier = Math.max(++state.retryMultiplier, 3)
|
||||||
|
},
|
||||||
|
resetRetryMultiplier (state) {
|
||||||
|
state.retryMultiplier = 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
// Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets
|
/**
|
||||||
enableMastoSockets (store) {
|
* Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets
|
||||||
const { state, dispatch } = store
|
*
|
||||||
if (state.mastoUserSocket) return
|
* @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')
|
return dispatch('startMastoUserSocket')
|
||||||
},
|
},
|
||||||
disableMastoSockets (store) {
|
disableMastoSockets (store) {
|
||||||
const { state, dispatch } = store
|
const { state, dispatch, commit } = store
|
||||||
if (!state.mastoUserSocket) return
|
if (!state.mastoUserSocket) return
|
||||||
|
commit('setMastoUserSocketStatus', WSConnectionStatus.DISABLED)
|
||||||
return dispatch('stopMastoUserSocket')
|
return dispatch('stopMastoUserSocket')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -91,11 +119,29 @@ const api = {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
state.mastoUserSocket.addEventListener('open', () => {
|
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)
|
commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED)
|
||||||
})
|
})
|
||||||
state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
|
state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
|
||||||
console.error('Error in MastoAPI websocket:', error)
|
console.error('Error in MastoAPI websocket:', error)
|
||||||
commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR)
|
// TODO is this needed?
|
||||||
dispatch('clearOpenedChats')
|
dispatch('clearOpenedChats')
|
||||||
})
|
})
|
||||||
state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
|
state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
|
||||||
|
@ -106,14 +152,26 @@ const api = {
|
||||||
const { code } = closeEvent
|
const { code } = closeEvent
|
||||||
if (ignoreCodes.has(code)) {
|
if (ignoreCodes.has(code)) {
|
||||||
console.debug(`Not restarting socket becasue of closure code ${code} is in ignore list`)
|
console.debug(`Not restarting socket becasue of closure code ${code} is in ignore list`)
|
||||||
|
commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED)
|
||||||
} else {
|
} else {
|
||||||
console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
|
console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
|
||||||
dispatch('startFetchingTimeline', { timeline: 'friends' })
|
setTimeout(() => {
|
||||||
dispatch('startFetchingNotifications')
|
dispatch('startMastoUserSocket')
|
||||||
dispatch('startFetchingChats')
|
}, retryTimeout(state.retryMultiplier))
|
||||||
dispatch('restartMastoUserSocket')
|
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)
|
||||||
}
|
}
|
||||||
commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED)
|
|
||||||
dispatch('clearOpenedChats')
|
dispatch('clearOpenedChats')
|
||||||
})
|
})
|
||||||
resolve()
|
resolve()
|
||||||
|
@ -122,15 +180,6 @@ const api = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
restartMastoUserSocket ({ dispatch }) {
|
|
||||||
// This basically starts MastoAPI user socket and stops conventional
|
|
||||||
// fetchers when connection reestablished
|
|
||||||
return dispatch('startMastoUserSocket').then(() => {
|
|
||||||
dispatch('stopFetchingTimeline', { timeline: 'friends' })
|
|
||||||
dispatch('stopFetchingNotifications')
|
|
||||||
dispatch('stopFetchingChats')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
stopMastoUserSocket ({ state, dispatch }) {
|
stopMastoUserSocket ({ state, dispatch }) {
|
||||||
dispatch('startFetchingTimeline', { timeline: 'friends' })
|
dispatch('startFetchingTimeline', { timeline: 'friends' })
|
||||||
dispatch('startFetchingNotifications')
|
dispatch('startFetchingNotifications')
|
||||||
|
@ -156,6 +205,13 @@ const api = {
|
||||||
if (!fetcher) return
|
if (!fetcher) return
|
||||||
store.commit('removeFetcher', { fetcherName: timeline, fetcher })
|
store.commit('removeFetcher', { fetcherName: timeline, fetcher })
|
||||||
},
|
},
|
||||||
|
fetchTimeline (store, timeline, { ...rest }) {
|
||||||
|
store.state.backendInteractor.fetchTimeline({
|
||||||
|
store,
|
||||||
|
timeline,
|
||||||
|
...rest
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
startFetchingNotifications (store) {
|
startFetchingNotifications (store) {
|
||||||
|
@ -168,6 +224,12 @@ const api = {
|
||||||
if (!fetcher) return
|
if (!fetcher) return
|
||||||
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
|
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
|
||||||
},
|
},
|
||||||
|
fetchNotifications (store, { ...rest }) {
|
||||||
|
store.state.backendInteractor.fetchNotifications({
|
||||||
|
store,
|
||||||
|
...rest
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
// Follow requests
|
// Follow requests
|
||||||
startFetchingFollowRequests (store) {
|
startFetchingFollowRequests (store) {
|
||||||
|
|
|
@ -547,9 +547,10 @@ const users = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (store.getters.mergedConfig.useStreamingApi) {
|
if (store.getters.mergedConfig.useStreamingApi) {
|
||||||
store.dispatch('enableMastoSockets').catch((error) => {
|
store.dispatch('fetchTimeline', 'friends', { since: null })
|
||||||
|
store.dispatch('fetchNotifications', { since: null })
|
||||||
|
store.dispatch('enableMastoSockets', true).catch((error) => {
|
||||||
console.error('Failed initializing MastoAPI Streaming socket', error)
|
console.error('Failed initializing MastoAPI Streaming socket', error)
|
||||||
startPolling()
|
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
store.dispatch('fetchChats', { latest: true })
|
store.dispatch('fetchChats', { latest: true })
|
||||||
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
|
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
|
||||||
|
|
|
@ -1152,6 +1152,7 @@ export const ProcessedWS = ({
|
||||||
|
|
||||||
// 1000 = Normal Closure
|
// 1000 = Normal Closure
|
||||||
eventTarget.close = () => { socket.close(1000, 'Shutting down socket') }
|
eventTarget.close = () => { socket.close(1000, 'Shutting down socket') }
|
||||||
|
eventTarget.getState = () => socket.readyState
|
||||||
|
|
||||||
return eventTarget
|
return eventTarget
|
||||||
}
|
}
|
||||||
|
@ -1183,7 +1184,10 @@ export const handleMastoWS = (wsEvent) => {
|
||||||
export const WSConnectionStatus = Object.freeze({
|
export const WSConnectionStatus = Object.freeze({
|
||||||
'JOINED': 1,
|
'JOINED': 1,
|
||||||
'CLOSED': 2,
|
'CLOSED': 2,
|
||||||
'ERROR': 3
|
'ERROR': 3,
|
||||||
|
'DISABLED': 4,
|
||||||
|
'STARTING': 5,
|
||||||
|
'STARTING_INITIAL': 6
|
||||||
})
|
})
|
||||||
|
|
||||||
const chats = ({ credentials }) => {
|
const chats = ({ credentials }) => {
|
||||||
|
|
|
@ -1,17 +1,25 @@
|
||||||
import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js'
|
import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js'
|
||||||
import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js'
|
import timelineFetcher from '../timeline_fetcher/timeline_fetcher.service.js'
|
||||||
import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
|
import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
|
||||||
import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
|
import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
|
||||||
|
|
||||||
const backendInteractorService = credentials => ({
|
const backendInteractorService = credentials => ({
|
||||||
startFetchingTimeline ({ timeline, store, userId = false, tag }) {
|
startFetchingTimeline ({ timeline, store, userId = false, tag }) {
|
||||||
return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag })
|
return timelineFetcher.startFetching({ timeline, store, credentials, userId, tag })
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchTimeline (args) {
|
||||||
|
return timelineFetcher.fetchAndUpdate({ ...args, credentials })
|
||||||
},
|
},
|
||||||
|
|
||||||
startFetchingNotifications ({ store }) {
|
startFetchingNotifications ({ store }) {
|
||||||
return notificationsFetcher.startFetching({ store, credentials })
|
return notificationsFetcher.startFetching({ store, credentials })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fetchNotifications (args) {
|
||||||
|
return notificationsFetcher.fetchAndUpdate({ ...args, credentials })
|
||||||
|
},
|
||||||
|
|
||||||
startFetchingFollowRequests ({ store }) {
|
startFetchingFollowRequests ({ store }) {
|
||||||
return followRequestFetcher.startFetching({ store, credentials })
|
return followRequestFetcher.startFetching({ store, credentials })
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,7 @@ const update = ({ store, notifications, older }) => {
|
||||||
store.dispatch('addNewNotifications', { notifications, older })
|
store.dispatch('addNewNotifications', { notifications, older })
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchAndUpdate = ({ store, credentials, older = false }) => {
|
const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
|
||||||
const args = { credentials }
|
const args = { credentials }
|
||||||
const { getters } = store
|
const { getters } = store
|
||||||
const rootState = store.rootState || store.state
|
const rootState = store.rootState || store.state
|
||||||
|
@ -22,8 +22,10 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
|
||||||
return fetchNotifications({ store, args, older })
|
return fetchNotifications({ store, args, older })
|
||||||
} else {
|
} else {
|
||||||
// fetch new notifications
|
// fetch new notifications
|
||||||
if (timelineData.maxId !== Number.POSITIVE_INFINITY) {
|
if (since === undefined && timelineData.maxId !== Number.POSITIVE_INFINITY) {
|
||||||
args['since'] = timelineData.maxId
|
args['since'] = timelineData.maxId
|
||||||
|
} else if (since !== null) {
|
||||||
|
args['since'] = since
|
||||||
}
|
}
|
||||||
const result = fetchNotifications({ store, args, older })
|
const result = fetchNotifications({ store, args, older })
|
||||||
|
|
||||||
|
|
|
@ -616,6 +616,23 @@ export const SLOT_INHERITANCE = {
|
||||||
textColor: true
|
textColor: true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
alertSuccess: {
|
||||||
|
depends: ['cGreen'],
|
||||||
|
opacity: 'alert'
|
||||||
|
},
|
||||||
|
alertSuccessText: {
|
||||||
|
depends: ['text'],
|
||||||
|
layer: 'alert',
|
||||||
|
variant: 'alertSuccess',
|
||||||
|
textColor: true
|
||||||
|
},
|
||||||
|
alertSuccessPanelText: {
|
||||||
|
depends: ['panelText'],
|
||||||
|
layer: 'alertPanel',
|
||||||
|
variant: 'alertSuccess',
|
||||||
|
textColor: true
|
||||||
|
},
|
||||||
|
|
||||||
alertNeutral: {
|
alertNeutral: {
|
||||||
depends: ['text'],
|
depends: ['text'],
|
||||||
opacity: 'alert'
|
opacity: 'alert'
|
||||||
|
@ -656,6 +673,17 @@ export const SLOT_INHERITANCE = {
|
||||||
textColor: true
|
textColor: true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
alertPopupSuccess: {
|
||||||
|
depends: ['alertSuccess'],
|
||||||
|
opacity: 'alertPopup'
|
||||||
|
},
|
||||||
|
alertPopupSuccessText: {
|
||||||
|
depends: ['alertSuccessText'],
|
||||||
|
layer: 'popover',
|
||||||
|
variant: 'alertPopupSuccess',
|
||||||
|
textColor: true
|
||||||
|
},
|
||||||
|
|
||||||
alertPopupNeutral: {
|
alertPopupNeutral: {
|
||||||
depends: ['alertNeutral'],
|
depends: ['alertNeutral'],
|
||||||
opacity: 'alertPopup'
|
opacity: 'alertPopup'
|
||||||
|
|
|
@ -23,7 +23,8 @@ const fetchAndUpdate = ({
|
||||||
showImmediately = false,
|
showImmediately = false,
|
||||||
userId = false,
|
userId = false,
|
||||||
tag = false,
|
tag = false,
|
||||||
until
|
until,
|
||||||
|
since
|
||||||
}) => {
|
}) => {
|
||||||
const args = { timeline, credentials }
|
const args = { timeline, credentials }
|
||||||
const rootState = store.rootState || store.state
|
const rootState = store.rootState || store.state
|
||||||
|
@ -35,7 +36,11 @@ const fetchAndUpdate = ({
|
||||||
if (older) {
|
if (older) {
|
||||||
args['until'] = until || timelineData.minId
|
args['until'] = until || timelineData.minId
|
||||||
} else {
|
} else {
|
||||||
args['since'] = timelineData.maxId
|
if (since === undefined) {
|
||||||
|
args['since'] = timelineData.maxId
|
||||||
|
} else if (since !== null) {
|
||||||
|
args['since'] = since
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
args['userId'] = userId
|
args['userId'] = userId
|
||||||
|
|
Loading…
Reference in a new issue