commit: a00212a3bbc029439a27ba01895e01adf37a2db6
parent 184364c7e06a982b48bbe6d913f02a73dd428aef
Author: Shpuld Shpludson <shp@cock.li>
Date: Mon, 15 Mar 2021 09:45:38 +0000
Merge branch 'websocket-fixes' into 'develop'
Various websocket fixes
See merge request pleroma/pleroma-fe!1326
Diffstat:
13 files changed, 168 insertions(+), 35 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -37,6 +37,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed
- Follows/Followers tabs on user profiles now display the content properly.
- 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
- Don't filter own posts when they hit your wordfilter
diff --git a/config/index.js b/config/index.js
@@ -3,6 +3,11 @@ const path = require('path')
let settings = {}
try {
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(JSON.stringify(settings, null, 2))
} catch (e) {
diff --git a/src/App.scss b/src/App.scss
@@ -706,6 +706,15 @@ nav {
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 {
diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue
@@ -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 {
background-color: var(--alertPopupNeutral, $fallback--fg);
color: var(--alertPopupNeutralText, $fallback--text);
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
@@ -35,11 +35,6 @@ const Notifications = {
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
}
},
- created () {
- const store = this.$store
- const credentials = store.state.users.currentUser.credentials
- notificationsFetcher.fetchAndUpdate({ store, credentials })
- },
computed: {
mainClass () {
return this.minimalMode ? '' : 'panel panel-default'
diff --git a/src/i18n/en.json b/src/i18n/en.json
@@ -663,7 +663,9 @@
"reload": "Reload",
"up_to_date": "Up-to-date",
"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": {
"favorites": "Favorites",
diff --git a/src/modules/api.js b/src/modules/api.js
@@ -3,8 +3,11 @@ 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,
@@ -34,18 +37,43 @@ const api = {
},
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
- enableMastoSockets (store) {
- const { state, dispatch } = store
- if (state.mastoUserSocket) return
+ /**
+ * 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 } = store
+ const { state, dispatch, commit } = store
if (!state.mastoUserSocket) return
+ commit('setMastoUserSocketStatus', WSConnectionStatus.DISABLED)
return dispatch('stopMastoUserSocket')
},
@@ -91,11 +119,29 @@ const api = {
}
)
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)
- commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR)
+ // TODO is this needed?
dispatch('clearOpenedChats')
})
state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
@@ -106,14 +152,26 @@ const api = {
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}`)
- dispatch('startFetchingTimeline', { timeline: 'friends' })
- dispatch('startFetchingNotifications')
- dispatch('startFetchingChats')
- dispatch('restartMastoUserSocket')
+ 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)
}
- commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED)
dispatch('clearOpenedChats')
})
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 }) {
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
@@ -156,6 +205,13 @@ const api = {
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: timeline, fetcher })
},
+ fetchTimeline (store, timeline, { ...rest }) {
+ store.state.backendInteractor.fetchTimeline({
+ store,
+ timeline,
+ ...rest
+ })
+ },
// Notifications
startFetchingNotifications (store) {
@@ -168,6 +224,12 @@ const api = {
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
},
+ fetchNotifications (store, { ...rest }) {
+ store.state.backendInteractor.fetchNotifications({
+ store,
+ ...rest
+ })
+ },
// Follow requests
startFetchingFollowRequests (store) {
diff --git a/src/modules/users.js b/src/modules/users.js
@@ -547,9 +547,10 @@ const users = {
}
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)
- startPolling()
}).then(() => {
store.dispatch('fetchChats', { latest: true })
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
@@ -1152,6 +1152,7 @@ export const ProcessedWS = ({
// 1000 = Normal Closure
eventTarget.close = () => { socket.close(1000, 'Shutting down socket') }
+ eventTarget.getState = () => socket.readyState
return eventTarget
}
@@ -1183,7 +1184,10 @@ export const handleMastoWS = (wsEvent) => {
export const WSConnectionStatus = Object.freeze({
'JOINED': 1,
'CLOSED': 2,
- 'ERROR': 3
+ 'ERROR': 3,
+ 'DISABLED': 4,
+ 'STARTING': 5,
+ 'STARTING_INITIAL': 6
})
const chats = ({ credentials }) => {
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -1,17 +1,25 @@
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 followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
const backendInteractorService = credentials => ({
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 }) {
return notificationsFetcher.startFetching({ store, credentials })
},
+ fetchNotifications (args) {
+ return notificationsFetcher.fetchAndUpdate({ ...args, credentials })
+ },
+
startFetchingFollowRequests ({ store }) {
return followRequestFetcher.startFetching({ store, credentials })
},
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -5,7 +5,7 @@ const update = ({ store, notifications, older }) => {
store.dispatch('addNewNotifications', { notifications, older })
}
-const fetchAndUpdate = ({ store, credentials, older = false }) => {
+const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
const args = { credentials }
const { getters } = store
const rootState = store.rootState || store.state
@@ -22,8 +22,10 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
return fetchNotifications({ store, args, older })
} else {
// fetch new notifications
- if (timelineData.maxId !== Number.POSITIVE_INFINITY) {
+ if (since === undefined && timelineData.maxId !== Number.POSITIVE_INFINITY) {
args['since'] = timelineData.maxId
+ } else if (since !== null) {
+ args['since'] = since
}
const result = fetchNotifications({ store, args, older })
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
@@ -616,6 +616,23 @@ export const SLOT_INHERITANCE = {
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: {
depends: ['text'],
opacity: 'alert'
@@ -656,6 +673,17 @@ export const SLOT_INHERITANCE = {
textColor: true
},
+ alertPopupSuccess: {
+ depends: ['alertSuccess'],
+ opacity: 'alertPopup'
+ },
+ alertPopupSuccessText: {
+ depends: ['alertSuccessText'],
+ layer: 'popover',
+ variant: 'alertPopupSuccess',
+ textColor: true
+ },
+
alertPopupNeutral: {
depends: ['alertNeutral'],
opacity: 'alertPopup'
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -23,7 +23,8 @@ const fetchAndUpdate = ({
showImmediately = false,
userId = false,
tag = false,
- until
+ until,
+ since
}) => {
const args = { timeline, credentials }
const rootState = store.rootState || store.state
@@ -35,7 +36,11 @@ const fetchAndUpdate = ({
if (older) {
args['until'] = until || timelineData.minId
} else {
- args['since'] = timelineData.maxId
+ if (since === undefined) {
+ args['since'] = timelineData.maxId
+ } else if (since !== null) {
+ args['since'] = since
+ }
}
args['userId'] = userId