Merge branch 'websocket-fixes' into 'develop'

Various websocket fixes

See merge request pleroma/pleroma-fe!1326
This commit is contained in:
Shpuld Shpludson 2021-03-15 09:45:38 +00:00
commit a00212a3bb
13 changed files with 168 additions and 35 deletions

View file

@ -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

View file

@ -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) {

View file

@ -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 {

View file

@ -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);

View file

@ -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'

View file

@ -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",

View file

@ -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}`)
setTimeout(() => {
dispatch('startMastoUserSocket')
}, retryTimeout(state.retryMultiplier))
commit('incrementRetryMultiplier')
if (state.mastoUserSocketStatus !== WSConnectionStatus.ERROR) {
dispatch('startFetchingTimeline', { timeline: 'friends' }) dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications') dispatch('startFetchingNotifications')
dispatch('startFetchingChats') dispatch('startFetchingChats')
dispatch('restartMastoUserSocket') 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) {

View file

@ -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)

View file

@ -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 }) => {

View file

@ -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 })
}, },

View file

@ -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 })

View file

@ -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'

View file

@ -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 {
if (since === undefined) {
args['since'] = timelineData.maxId args['since'] = timelineData.maxId
} else if (since !== null) {
args['since'] = since
}
} }
args['userId'] = userId args['userId'] = userId