diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js
index 1a00a1c1..a8931787 100644
--- a/src/components/follow_request_card/follow_request_card.js
+++ b/src/components/follow_request_card/follow_request_card.js
@@ -7,11 +7,11 @@ const FollowRequestCard = {
},
methods: {
approveUser () {
- this.$store.state.api.backendInteractor.approveUser(this.user.id)
+ this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user)
},
denyUser () {
- this.$store.state.api.backendInteractor.denyUser(this.user.id)
+ this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user)
}
}
diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js
index 02b92fef..757166ed 100644
--- a/src/components/moderation_tools/moderation_tools.js
+++ b/src/components/moderation_tools/moderation_tools.js
@@ -45,12 +45,12 @@ const ModerationTools = {
toggleTag (tag) {
const store = this.$store
if (this.tagsSet.has(tag)) {
- store.state.api.backendInteractor.untagUser(this.user, tag).then(response => {
+ store.state.api.backendInteractor.untagUser({ user: this.user, tag }).then(response => {
if (!response.ok) { return }
store.commit('untagUser', { user: this.user, tag })
})
} else {
- store.state.api.backendInteractor.tagUser(this.user, tag).then(response => {
+ store.state.api.backendInteractor.tagUser({ user: this.user, tag }).then(response => {
if (!response.ok) { return }
store.commit('tagUser', { user: this.user, tag })
})
@@ -59,19 +59,19 @@ const ModerationTools = {
toggleRight (right) {
const store = this.$store
if (this.user.rights[right]) {
- store.state.api.backendInteractor.deleteRight(this.user, right).then(response => {
+ store.state.api.backendInteractor.deleteRight({ user: this.user, right }).then(response => {
if (!response.ok) { return }
- store.commit('updateRight', { user: this.user, right: right, value: false })
+ store.commit('updateRight', { user: this.user, right, value: false })
})
} else {
- store.state.api.backendInteractor.addRight(this.user, right).then(response => {
+ store.state.api.backendInteractor.addRight({ user: this.user, right }).then(response => {
if (!response.ok) { return }
- store.commit('updateRight', { user: this.user, right: right, value: true })
+ store.commit('updateRight', { user: this.user, right, value: true })
})
}
},
toggleActivationStatus () {
- this.$store.dispatch('toggleActivationStatus', this.user)
+ this.$store.dispatch('toggleActivationStatus', { user: this.user })
},
deleteUserDialog (show) {
this.showDeleteUserDialog = show
@@ -80,7 +80,7 @@ const ModerationTools = {
const store = this.$store
const user = this.user
const { id, name } = user
- store.state.api.backendInteractor.deleteUser(user)
+ store.state.api.backendInteractor.deleteUser({ user })
.then(e => {
this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id)
const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index 6c4054fd..a89c0cdc 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -47,6 +47,11 @@ const Notifications = {
components: {
Notification
},
+ created () {
+ const { dispatch } = this.$store
+
+ dispatch('fetchAndUpdateNotifications')
+ },
watch: {
unseenCount (count) {
if (count > 0) {
diff --git a/src/components/public_and_external_timeline/public_and_external_timeline.js b/src/components/public_and_external_timeline/public_and_external_timeline.js
index f614c13b..cbd4491b 100644
--- a/src/components/public_and_external_timeline/public_and_external_timeline.js
+++ b/src/components/public_and_external_timeline/public_and_external_timeline.js
@@ -10,7 +10,7 @@ const PublicAndExternalTimeline = {
this.$store.dispatch('startFetchingTimeline', { timeline: 'publicAndExternal' })
},
destroyed () {
- this.$store.dispatch('stopFetching', 'publicAndExternal')
+ this.$store.dispatch('stopFetchingTimeline', 'publicAndExternal')
}
}
diff --git a/src/components/public_timeline/public_timeline.js b/src/components/public_timeline/public_timeline.js
index 8976a99c..66c40d3a 100644
--- a/src/components/public_timeline/public_timeline.js
+++ b/src/components/public_timeline/public_timeline.js
@@ -10,7 +10,7 @@ const PublicTimeline = {
this.$store.dispatch('startFetchingTimeline', { timeline: 'public' })
},
destroyed () {
- this.$store.dispatch('stopFetching', 'public')
+ this.$store.dispatch('stopFetchingTimeline', 'public')
}
}
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index c49083f9..31a9e9be 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -84,7 +84,7 @@ const settings = {
}
}])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
- // Special cases (need to transform values)
+ // Special cases (need to transform values or perform actions first)
muteWordsString: {
get () { return this.$store.getters.mergedConfig.muteWords.join('\n') },
set (value) {
@@ -93,6 +93,22 @@ const settings = {
value: filter(value.split('\n'), (word) => trim(word).length > 0)
})
}
+ },
+ useStreamingApi: {
+ get () { return this.$store.getters.mergedConfig.useStreamingApi },
+ set (value) {
+ const promise = value
+ ? this.$store.dispatch('enableMastoSockets')
+ : this.$store.dispatch('disableMastoSockets')
+
+ promise.then(() => {
+ this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
+ }).catch((e) => {
+ console.error('Failed starting MastoAPI Streaming socket', e)
+ this.$store.dispatch('disableMastoSockets')
+ this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
+ })
+ }
}
},
// Updating nested properties
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index c4021137..b40c85dd 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -73,6 +73,15 @@
+
+
+ {{ $t('settings.useStreamingApi') }}
+
+
+ {{ $t('settings.useStreamingApiWarning') }}
+
+
+
{{ $t('settings.autoload') }}
diff --git a/src/components/tag_timeline/tag_timeline.js b/src/components/tag_timeline/tag_timeline.js
index 458eb1c5..400c6a4b 100644
--- a/src/components/tag_timeline/tag_timeline.js
+++ b/src/components/tag_timeline/tag_timeline.js
@@ -19,7 +19,7 @@ const TagTimeline = {
}
},
destroyed () {
- this.$store.dispatch('stopFetching', 'tag')
+ this.$store.dispatch('stopFetchingTimeline', 'tag')
}
}
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 00055707..9558a0bd 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -112,9 +112,9 @@ const UserProfile = {
}
},
stopFetching () {
- this.$store.dispatch('stopFetching', 'user')
- this.$store.dispatch('stopFetching', 'favorites')
- this.$store.dispatch('stopFetching', 'media')
+ this.$store.dispatch('stopFetchingTimeline', 'user')
+ this.$store.dispatch('stopFetchingTimeline', 'favorites')
+ this.$store.dispatch('stopFetchingTimeline', 'media')
},
switchUser (userNameOrId) {
this.stopFetching()
diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js
index 833fa98a..38cf117b 100644
--- a/src/components/user_reporting_modal/user_reporting_modal.js
+++ b/src/components/user_reporting_modal/user_reporting_modal.js
@@ -64,7 +64,7 @@ const UserReportingModal = {
forward: this.forward,
statusIds: this.statusIdsToReport
}
- this.$store.state.api.backendInteractor.reportUser(params)
+ this.$store.state.api.backendInteractor.reportUser({ ...params })
.then(() => {
this.processing = false
this.resetState()
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 3fdc5340..d5d671e4 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -242,7 +242,7 @@ const UserSettings = {
})
},
importFollows (file) {
- return this.$store.state.api.backendInteractor.importFollows(file)
+ return this.$store.state.api.backendInteractor.importFollows({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
@@ -250,7 +250,7 @@ const UserSettings = {
})
},
importBlocks (file) {
- return this.$store.state.api.backendInteractor.importBlocks(file)
+ return this.$store.state.api.backendInteractor.importBlocks({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
@@ -297,7 +297,7 @@ const UserSettings = {
newPassword: this.changePasswordInputs[1],
newPasswordConfirmation: this.changePasswordInputs[2]
}
- this.$store.state.api.backendInteractor.changePassword(params)
+ this.$store.state.api.backendInteractor.changePassword({ params })
.then((res) => {
if (res.status === 'success') {
this.changedPassword = true
@@ -314,7 +314,7 @@ const UserSettings = {
email: this.newEmail,
password: this.changeEmailPassword
}
- this.$store.state.api.backendInteractor.changeEmail(params)
+ this.$store.state.api.backendInteractor.changeEmail({ params })
.then((res) => {
if (res.status === 'success') {
this.changedEmail = true
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 85146ef5..60fc792f 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -358,6 +358,8 @@
"post_status_content_type": "Post status content type",
"stop_gifs": "Play-on-hover GIFs",
"streaming": "Enable automatic streaming of new posts when scrolled to the top",
+ "useStreamingApi": "Receive posts and notifications real-time",
+ "useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
"text": "Text",
"theme": "Theme",
"theme_help": "Use hex color codes (#rrggbb) to customize your color theme.",
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 19e10f1e..4cb2d497 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -219,6 +219,8 @@
"subject_input_always_show": "Всегда показывать поле ввода темы",
"stop_gifs": "Проигрывать GIF анимации только при наведении",
"streaming": "Включить автоматическую загрузку новых сообщений при прокрутке вверх",
+ "useStreamingApi": "Получать сообщения и уведомления в реальном времени",
+ "useStreamingApiWarning": "(Не рекомендуется, экспериментально, сообщения могут пропадать)",
"text": "Текст",
"theme": "Тема",
"theme_help": "Используйте шестнадцатеричные коды цветов (#rrggbb) для настройки темы.",
diff --git a/src/modules/api.js b/src/modules/api.js
index 1293e3c8..9c296275 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -6,6 +6,7 @@ const api = {
backendInteractor: backendInteractorService(),
fetchers: {},
socket: null,
+ mastoUserSocket: null,
followRequests: []
},
mutations: {
@@ -15,7 +16,8 @@ const api = {
addFetcher (state, { fetcherName, fetcher }) {
state.fetchers[fetcherName] = fetcher
},
- removeFetcher (state, { fetcherName }) {
+ removeFetcher (state, { fetcherName, fetcher }) {
+ window.clearInterval(fetcher)
delete state.fetchers[fetcherName]
},
setWsToken (state, token) {
@@ -29,32 +31,134 @@ const api = {
}
},
actions: {
- startFetchingTimeline (store, { timeline = 'friends', tag = false, userId = false }) {
- // Don't start fetching if we already are.
+ // Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets
+ enableMastoSockets (store) {
+ const { state, dispatch } = store
+ if (state.mastoUserSocket) return
+ return dispatch('startMastoUserSocket')
+ },
+ disableMastoSockets (store) {
+ const { state, dispatch } = store
+ if (!state.mastoUserSocket) return
+ return dispatch('stopMastoUserSocket')
+ },
+
+ // MastoAPI 'User' sockets
+ startMastoUserSocket (store) {
+ return new Promise((resolve, reject) => {
+ try {
+ const { state, 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'
+ })
+ }
+ }
+ )
+ state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
+ console.error('Error in MastoAPI websocket:', error)
+ })
+ 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`)
+ } else {
+ console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
+ dispatch('startFetchingTimeline', { timeline: 'friends' })
+ dispatch('startFetchingNotifications')
+ dispatch('restartMastoUserSocket')
+ }
+ })
+ resolve()
+ } catch (e) {
+ reject(e)
+ }
+ })
+ },
+ 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')
+ })
+ },
+ stopMastoUserSocket ({ state, dispatch }) {
+ dispatch('startFetchingTimeline', { timeline: 'friends' })
+ dispatch('startFetchingNotifications')
+ console.log(state.mastoUserSocket)
+ state.mastoUserSocket.close()
+ },
+
+ // Timelines
+ startFetchingTimeline (store, {
+ timeline = 'friends',
+ tag = false,
+ userId = false
+ }) {
if (store.state.fetchers[timeline]) return
- const fetcher = store.state.backendInteractor.startFetchingTimeline({ timeline, store, userId, tag })
+ const fetcher = store.state.backendInteractor.startFetchingTimeline({
+ timeline, store, userId, tag
+ })
store.commit('addFetcher', { fetcherName: timeline, fetcher })
},
- startFetchingNotifications (store) {
- // Don't start fetching if we already are.
- if (store.state.fetchers['notifications']) return
+ stopFetchingTimeline (store, timeline) {
+ const fetcher = store.state.fetchers[timeline]
+ if (!fetcher) return
+ store.commit('removeFetcher', { fetcherName: timeline, fetcher })
+ },
+ // Notifications
+ startFetchingNotifications (store) {
+ if (store.state.fetchers.notifications) return
const fetcher = store.state.backendInteractor.startFetchingNotifications({ store })
store.commit('addFetcher', { fetcherName: 'notifications', fetcher })
},
- startFetchingFollowRequest (store) {
- // Don't start fetching if we already are.
- if (store.state.fetchers['followRequest']) return
+ stopFetchingNotifications (store) {
+ const fetcher = store.state.fetchers.notifications
+ if (!fetcher) return
+ store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
+ },
+ fetchAndUpdateNotifications (store) {
+ store.state.backendInteractor.fetchAndUpdateNotifications({ store })
+ },
- const fetcher = store.state.backendInteractor.startFetchingFollowRequest({ store })
- store.commit('addFetcher', { fetcherName: 'followRequest', fetcher })
+ // Follow requests
+ startFetchingFollowRequests (store) {
+ if (store.state.fetchers['followRequests']) return
+ const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
+ store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
},
- stopFetching (store, fetcherName) {
- const fetcher = store.state.fetchers[fetcherName]
- window.clearInterval(fetcher)
- store.commit('removeFetcher', { fetcherName })
+ 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)
},
@@ -72,10 +176,6 @@ const api = {
disconnectFromSocket ({ commit, state }) {
state.socket && state.socket.disconnect()
commit('setSocket', null)
- },
- removeFollowRequest (store, request) {
- let requests = store.state.followRequests.filter((it) => it !== request)
- store.commit('setFollowRequests', requests)
}
}
}
diff --git a/src/modules/config.js b/src/modules/config.js
index 329b4091..74025db1 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -35,6 +35,7 @@ export const defaultState = {
highlight: {},
interfaceLanguage: browserLocale,
hideScopeNotice: false,
+ useStreamingApi: false,
scopeCopy: undefined, // instance default
subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined, // instance default
diff --git a/src/modules/oauth_tokens.js b/src/modules/oauth_tokens.js
index 0159a3f1..907cae4a 100644
--- a/src/modules/oauth_tokens.js
+++ b/src/modules/oauth_tokens.js
@@ -9,7 +9,7 @@ const oauthTokens = {
})
},
revokeToken ({ rootState, commit, state }, id) {
- rootState.api.backendInteractor.revokeOAuthToken(id).then((response) => {
+ rootState.api.backendInteractor.revokeOAuthToken({ id }).then((response) => {
if (response.status === 201) {
commit('swapTokens', state.tokens.filter(token => token.id !== id))
}
diff --git a/src/modules/polls.js b/src/modules/polls.js
index e6158b63..92b89a06 100644
--- a/src/modules/polls.js
+++ b/src/modules/polls.js
@@ -40,7 +40,7 @@ const polls = {
commit('mergeOrAddPoll', poll)
},
updateTrackedPoll ({ rootState, dispatch, commit }, pollId) {
- rootState.api.backendInteractor.fetchPoll(pollId).then(poll => {
+ rootState.api.backendInteractor.fetchPoll({ pollId }).then(poll => {
setTimeout(() => {
if (rootState.polls.trackedPolls[pollId]) {
dispatch('updateTrackedPoll', pollId)
@@ -59,7 +59,7 @@ const polls = {
commit('untrackPoll', pollId)
},
votePoll ({ rootState, commit }, { id, pollId, choices }) {
- return rootState.api.backendInteractor.vote(pollId, choices).then(poll => {
+ return rootState.api.backendInteractor.vote({ pollId, choices }).then(poll => {
commit('mergeOrAddPoll', poll)
return poll
})
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index e3a1f293..7d88761c 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -558,45 +558,45 @@ const statuses = {
favorite ({ rootState, commit }, status) {
// Optimistic favoriting...
commit('setFavorited', { status, value: true })
- rootState.api.backendInteractor.favorite(status.id)
+ rootState.api.backendInteractor.favorite({ id: status.id })
.then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
},
unfavorite ({ rootState, commit }, status) {
// Optimistic unfavoriting...
commit('setFavorited', { status, value: false })
- rootState.api.backendInteractor.unfavorite(status.id)
+ rootState.api.backendInteractor.unfavorite({ id: status.id })
.then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
},
fetchPinnedStatuses ({ rootState, dispatch }, userId) {
- rootState.api.backendInteractor.fetchPinnedStatuses(userId)
+ rootState.api.backendInteractor.fetchPinnedStatuses({ id: userId })
.then(statuses => dispatch('addNewStatuses', { statuses, timeline: 'user', userId, showImmediately: true, noIdUpdate: true }))
},
pinStatus ({ rootState, dispatch }, statusId) {
- return rootState.api.backendInteractor.pinOwnStatus(statusId)
+ return rootState.api.backendInteractor.pinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
unpinStatus ({ rootState, dispatch }, statusId) {
- rootState.api.backendInteractor.unpinOwnStatus(statusId)
+ rootState.api.backendInteractor.unpinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
muteConversation ({ rootState, commit }, statusId) {
- return rootState.api.backendInteractor.muteConversation(statusId)
+ return rootState.api.backendInteractor.muteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status))
},
unmuteConversation ({ rootState, commit }, statusId) {
- return rootState.api.backendInteractor.unmuteConversation(statusId)
+ return rootState.api.backendInteractor.unmuteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status))
},
retweet ({ rootState, commit }, status) {
// Optimistic retweeting...
commit('setRetweeted', { status, value: true })
- rootState.api.backendInteractor.retweet(status.id)
+ rootState.api.backendInteractor.retweet({ id: status.id })
.then(status => commit('setRetweetedConfirm', { status: status.retweeted_status, user: rootState.users.currentUser }))
},
unretweet ({ rootState, commit }, status) {
// Optimistic unretweeting...
commit('setRetweeted', { status, value: false })
- rootState.api.backendInteractor.unretweet(status.id)
+ rootState.api.backendInteractor.unretweet({ id: status.id })
.then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser }))
},
queueFlush ({ rootState, commit }, { timeline, id }) {
@@ -611,19 +611,19 @@ const statuses = {
},
fetchFavsAndRepeats ({ rootState, commit }, id) {
Promise.all([
- rootState.api.backendInteractor.fetchFavoritedByUsers(id),
- rootState.api.backendInteractor.fetchRebloggedByUsers(id)
+ rootState.api.backendInteractor.fetchFavoritedByUsers({ id }),
+ rootState.api.backendInteractor.fetchRebloggedByUsers({ id })
]).then(([favoritedByUsers, rebloggedByUsers]) => {
commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser })
commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser })
})
},
fetchFavs ({ rootState, commit }, id) {
- rootState.api.backendInteractor.fetchFavoritedByUsers(id)
+ rootState.api.backendInteractor.fetchFavoritedByUsers({ id })
.then(favoritedByUsers => commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser }))
},
fetchRepeats ({ rootState, commit }, id) {
- rootState.api.backendInteractor.fetchRebloggedByUsers(id)
+ rootState.api.backendInteractor.fetchRebloggedByUsers({ id })
.then(rebloggedByUsers => commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser }))
},
search (store, { q, resolve, limit, offset, following }) {
diff --git a/src/modules/users.js b/src/modules/users.js
index 82d3c4e8..e54588df 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -32,7 +32,7 @@ const getNotificationPermission = () => {
}
const blockUser = (store, id) => {
- return store.rootState.api.backendInteractor.blockUser(id)
+ return store.rootState.api.backendInteractor.blockUser({ id })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
store.commit('addBlockId', id)
@@ -43,12 +43,12 @@ const blockUser = (store, id) => {
}
const unblockUser = (store, id) => {
- return store.rootState.api.backendInteractor.unblockUser(id)
+ return store.rootState.api.backendInteractor.unblockUser({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const muteUser = (store, id) => {
- return store.rootState.api.backendInteractor.muteUser(id)
+ return store.rootState.api.backendInteractor.muteUser({ id })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
store.commit('addMuteId', id)
@@ -56,7 +56,7 @@ const muteUser = (store, id) => {
}
const unmuteUser = (store, id) => {
- return store.rootState.api.backendInteractor.unmuteUser(id)
+ return store.rootState.api.backendInteractor.unmuteUser({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
@@ -324,11 +324,11 @@ const users = {
commit('clearFollowers', userId)
},
subscribeUser ({ rootState, commit }, id) {
- return rootState.api.backendInteractor.subscribeUser(id)
+ return rootState.api.backendInteractor.subscribeUser({ id })
.then((relationship) => commit('updateUserRelationship', [relationship]))
},
unsubscribeUser ({ rootState, commit }, id) {
- return rootState.api.backendInteractor.unsubscribeUser(id)
+ return rootState.api.backendInteractor.unsubscribeUser({ id })
.then((relationship) => commit('updateUserRelationship', [relationship]))
},
toggleActivationStatus ({ rootState, commit }, user) {
@@ -387,7 +387,7 @@ const users = {
})
},
searchUsers (store, query) {
- return store.rootState.api.backendInteractor.searchUsers(query)
+ return store.rootState.api.backendInteractor.searchUsers({ query })
.then((users) => {
store.commit('addNewUsers', users)
return users
@@ -399,7 +399,7 @@ const users = {
let rootState = store.rootState
try {
- let data = await rootState.api.backendInteractor.register(userInfo)
+ let data = await rootState.api.backendInteractor.register({ ...userInfo })
store.commit('signUpSuccess')
store.commit('setToken', data.access_token)
store.dispatch('loginUser', data.access_token)
@@ -436,10 +436,10 @@ const users = {
store.commit('clearCurrentUser')
store.dispatch('disconnectFromSocket')
store.commit('clearToken')
- store.dispatch('stopFetching', 'friends')
+ store.dispatch('stopFetchingTimeline', 'friends')
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
- store.dispatch('stopFetching', 'notifications')
- store.dispatch('stopFetching', 'followRequest')
+ store.dispatch('stopFetchingNotifications')
+ store.dispatch('stopFetchingFollowRequests')
store.commit('clearNotifications')
store.commit('resetStatuses')
})
@@ -474,11 +474,24 @@ const users = {
store.dispatch('initializeSocket')
}
- // Start getting fresh posts.
- store.dispatch('startFetchingTimeline', { timeline: 'friends' })
+ const startPolling = () => {
+ // Start getting fresh posts.
+ store.dispatch('startFetchingTimeline', { timeline: 'friends' })
- // Start fetching notifications
- store.dispatch('startFetchingNotifications')
+ // Start fetching notifications
+ store.dispatch('startFetchingNotifications')
+ }
+
+ if (store.getters.mergedConfig.useStreamingApi) {
+ store.dispatch('enableMastoSockets').catch((error) => {
+ console.error('Failed initializing MastoAPI Streaming socket', error)
+ startPolling()
+ }).then(() => {
+ setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
+ })
+ } else {
+ startPolling()
+ }
// Get user mutes
store.dispatch('fetchMutes')
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index a69fa53c..ef0267aa 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -72,6 +72,7 @@ const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
const MASTODON_SEARCH_2 = `/api/v2/search`
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
+const MASTODON_STREAMING = '/api/v1/streaming'
const oldfetch = window.fetch
@@ -451,7 +452,7 @@ const deleteRight = ({ right, credentials, ...user }) => {
})
}
-const activateUser = ({ credentials, screen_name: nickname }) => {
+const activateUser = ({ credentials, user: { screen_name: nickname } }) => {
return promisedRequest({
url: ACTIVATE_USER_URL,
method: 'PATCH',
@@ -462,7 +463,7 @@ const activateUser = ({ credentials, screen_name: nickname }) => {
}).then(response => get(response, 'users.0'))
}
-const deactivateUser = ({ credentials, screen_name: nickname }) => {
+const deactivateUser = ({ credentials, user: { screen_name: nickname } }) => {
return promisedRequest({
url: DEACTIVATE_USER_URL,
method: 'PATCH',
@@ -947,6 +948,99 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
})
}
+export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
+ return Object.entries({
+ ...(credentials
+ ? { access_token: credentials }
+ : {}
+ ),
+ stream,
+ ...args
+ }).reduce((acc, [key, val]) => {
+ return acc + `${key}=${val}&`
+ }, MASTODON_STREAMING + '?')
+}
+
+const MASTODON_STREAMING_EVENTS = new Set([
+ 'update',
+ 'notification',
+ 'delete',
+ 'filters_changed'
+])
+
+// A thin wrapper around WebSocket API that allows adding a pre-processor to it
+// Uses EventTarget and a CustomEvent to proxy events
+export const ProcessedWS = ({
+ url,
+ preprocessor = handleMastoWS,
+ id = 'Unknown'
+}) => {
+ const eventTarget = new EventTarget()
+ const socket = new WebSocket(url)
+ if (!socket) throw new Error(`Failed to create socket ${id}`)
+ const proxy = (original, eventName, processor = a => a) => {
+ original.addEventListener(eventName, (eventData) => {
+ eventTarget.dispatchEvent(new CustomEvent(
+ eventName,
+ { detail: processor(eventData) }
+ ))
+ })
+ }
+ socket.addEventListener('open', (wsEvent) => {
+ console.debug(`[WS][${id}] Socket connected`, wsEvent)
+ })
+ socket.addEventListener('error', (wsEvent) => {
+ console.debug(`[WS][${id}] Socket errored`, wsEvent)
+ })
+ socket.addEventListener('close', (wsEvent) => {
+ console.debug(
+ `[WS][${id}] Socket disconnected with code ${wsEvent.code}`,
+ wsEvent
+ )
+ })
+ // Commented code reason: very spammy, uncomment to enable message debug logging
+ /*
+ socket.addEventListener('message', (wsEvent) => {
+ console.debug(
+ `[WS][${id}] Message received`,
+ wsEvent
+ )
+ })
+ /**/
+
+ proxy(socket, 'open')
+ proxy(socket, 'close')
+ proxy(socket, 'message', preprocessor)
+ proxy(socket, 'error')
+
+ // 1000 = Normal Closure
+ eventTarget.close = () => { socket.close(1000, 'Shutting down socket') }
+
+ return eventTarget
+}
+
+export const handleMastoWS = (wsEvent) => {
+ const { data } = wsEvent
+ if (!data) return
+ const parsedEvent = JSON.parse(data)
+ const { event, payload } = parsedEvent
+ if (MASTODON_STREAMING_EVENTS.has(event)) {
+ // MastoBE and PleromaBE both send payload for delete as a PLAIN string
+ if (event === 'delete') {
+ return { event, id: payload }
+ }
+ const data = payload ? JSON.parse(payload) : null
+ if (event === 'update') {
+ return { event, status: parseStatus(data) }
+ } else if (event === 'notification') {
+ return { event, notification: parseNotification(data) }
+ }
+ } else {
+ console.warn('Unknown event', wsEvent)
+ return null
+ }
+}
+
const apiService = {
verifyCredentials,
fetchTimeline,
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 3d4ec6ac..b7372ed0 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -1,236 +1,39 @@
-import apiService from '../api/api.service.js'
+import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js'
import timelineFetcherService 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 => {
- const fetchStatus = ({ id }) => {
- return apiService.fetchStatus({ id, credentials })
- }
-
- const fetchConversation = ({ id }) => {
- return apiService.fetchConversation({ id, credentials })
- }
-
- const fetchFriends = ({ id, maxId, sinceId, limit }) => {
- return apiService.fetchFriends({ id, maxId, sinceId, limit, credentials })
- }
-
- const exportFriends = ({ id }) => {
- return apiService.exportFriends({ id, credentials })
- }
-
- const fetchFollowers = ({ id, maxId, sinceId, limit }) => {
- return apiService.fetchFollowers({ id, maxId, sinceId, limit, credentials })
- }
-
- const fetchUser = ({ id }) => {
- return apiService.fetchUser({ id, credentials })
- }
-
- const fetchUserRelationship = ({ id }) => {
- return apiService.fetchUserRelationship({ id, credentials })
- }
-
- const followUser = ({ id, reblogs }) => {
- return apiService.followUser({ credentials, id, reblogs })
- }
-
- const unfollowUser = (id) => {
- return apiService.unfollowUser({ credentials, id })
- }
-
- const blockUser = (id) => {
- return apiService.blockUser({ credentials, id })
- }
-
- const unblockUser = (id) => {
- return apiService.unblockUser({ credentials, id })
- }
-
- const approveUser = (id) => {
- return apiService.approveUser({ credentials, id })
- }
-
- const denyUser = (id) => {
- return apiService.denyUser({ credentials, id })
- }
-
- const startFetchingTimeline = ({ timeline, store, userId = false, tag }) => {
+const backendInteractorService = credentials => ({
+ startFetchingTimeline ({ timeline, store, userId = false, tag }) {
return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag })
- }
+ },
- const startFetchingNotifications = ({ store }) => {
+ startFetchingNotifications ({ store }) {
return notificationsFetcher.startFetching({ store, credentials })
- }
+ },
- const startFetchingFollowRequest = ({ store }) => {
+ fetchAndUpdateNotifications ({ store }) {
+ return notificationsFetcher.fetchAndUpdate({ store, credentials })
+ },
+
+ startFetchingFollowRequest ({ store }) {
return followRequestFetcher.startFetching({ store, credentials })
- }
+ },
- // eslint-disable-next-line camelcase
- const tagUser = ({ screen_name }, tag) => {
- return apiService.tagUser({ screen_name, tag, credentials })
- }
+ startUserSocket ({ store }) {
+ const serv = store.rootState.instance.server.replace('http', 'ws')
+ const url = serv + getMastodonSocketURI({ credentials, stream: 'user' })
+ return ProcessedWS({ url, id: 'User' })
+ },
- // eslint-disable-next-line camelcase
- const untagUser = ({ screen_name }, tag) => {
- return apiService.untagUser({ screen_name, tag, credentials })
- }
+ ...Object.entries(apiService).reduce((acc, [key, func]) => {
+ return {
+ ...acc,
+ [key]: (args) => func({ credentials, ...args })
+ }
+ }, {}),
- // eslint-disable-next-line camelcase
- const addRight = ({ screen_name }, right) => {
- return apiService.addRight({ screen_name, right, credentials })
- }
-
- // eslint-disable-next-line camelcase
- const deleteRight = ({ screen_name }, right) => {
- return apiService.deleteRight({ screen_name, right, credentials })
- }
-
- // eslint-disable-next-line camelcase
- const activateUser = ({ screen_name }) => {
- return apiService.activateUser({ screen_name, credentials })
- }
-
- // eslint-disable-next-line camelcase
- const deactivateUser = ({ screen_name }) => {
- return apiService.deactivateUser({ screen_name, credentials })
- }
-
- // eslint-disable-next-line camelcase
- const deleteUser = ({ screen_name }) => {
- return apiService.deleteUser({ screen_name, credentials })
- }
-
- const vote = (pollId, choices) => {
- return apiService.vote({ credentials, pollId, choices })
- }
-
- const fetchPoll = (pollId) => {
- return apiService.fetchPoll({ credentials, pollId })
- }
-
- const updateNotificationSettings = ({ settings }) => {
- return apiService.updateNotificationSettings({ credentials, settings })
- }
-
- const fetchMutes = () => apiService.fetchMutes({ credentials })
- const muteUser = (id) => apiService.muteUser({ credentials, id })
- const unmuteUser = (id) => apiService.unmuteUser({ credentials, id })
- const subscribeUser = (id) => apiService.subscribeUser({ credentials, id })
- const unsubscribeUser = (id) => apiService.unsubscribeUser({ credentials, id })
- const fetchBlocks = () => apiService.fetchBlocks({ credentials })
- const fetchOAuthTokens = () => apiService.fetchOAuthTokens({ credentials })
- const revokeOAuthToken = (id) => apiService.revokeOAuthToken({ id, credentials })
- const fetchPinnedStatuses = (id) => apiService.fetchPinnedStatuses({ credentials, id })
- const pinOwnStatus = (id) => apiService.pinOwnStatus({ credentials, id })
- const unpinOwnStatus = (id) => apiService.unpinOwnStatus({ credentials, id })
- const muteConversation = (id) => apiService.muteConversation({ credentials, id })
- const unmuteConversation = (id) => apiService.unmuteConversation({ credentials, id })
-
- const getCaptcha = () => apiService.getCaptcha()
- const register = (params) => apiService.register({ credentials, params })
- const updateAvatar = ({ avatar }) => apiService.updateAvatar({ credentials, avatar })
- const updateBg = ({ background }) => apiService.updateBg({ credentials, background })
- const updateBanner = ({ banner }) => apiService.updateBanner({ credentials, banner })
- const updateProfile = ({ params }) => apiService.updateProfile({ credentials, params })
-
- const importBlocks = (file) => apiService.importBlocks({ file, credentials })
- const importFollows = (file) => apiService.importFollows({ file, credentials })
-
- const deleteAccount = ({ password }) => apiService.deleteAccount({ credentials, password })
- const changeEmail = ({ email, password }) => apiService.changeEmail({ credentials, email, password })
- const changePassword = ({ password, newPassword, newPasswordConfirmation }) =>
- apiService.changePassword({ credentials, password, newPassword, newPasswordConfirmation })
-
- const fetchSettingsMFA = () => apiService.settingsMFA({ credentials })
- const generateMfaBackupCodes = () => apiService.generateMfaBackupCodes({ credentials })
- const mfaSetupOTP = () => apiService.mfaSetupOTP({ credentials })
- const mfaConfirmOTP = ({ password, token }) => apiService.mfaConfirmOTP({ credentials, password, token })
- const mfaDisableOTP = ({ password }) => apiService.mfaDisableOTP({ credentials, password })
-
- const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({ id })
- const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({ id })
- const reportUser = (params) => apiService.reportUser({ credentials, ...params })
-
- const favorite = (id) => apiService.favorite({ id, credentials })
- const unfavorite = (id) => apiService.unfavorite({ id, credentials })
- const retweet = (id) => apiService.retweet({ id, credentials })
- const unretweet = (id) => apiService.unretweet({ id, credentials })
- const search2 = ({ q, resolve, limit, offset, following }) =>
- apiService.search2({ credentials, q, resolve, limit, offset, following })
- const searchUsers = (query) => apiService.searchUsers({ query, credentials })
-
- const backendInteractorServiceInstance = {
- fetchStatus,
- fetchConversation,
- fetchFriends,
- exportFriends,
- fetchFollowers,
- followUser,
- unfollowUser,
- blockUser,
- unblockUser,
- fetchUser,
- fetchUserRelationship,
- verifyCredentials: apiService.verifyCredentials,
- startFetchingTimeline,
- startFetchingNotifications,
- startFetchingFollowRequest,
- fetchMutes,
- muteUser,
- unmuteUser,
- subscribeUser,
- unsubscribeUser,
- fetchBlocks,
- fetchOAuthTokens,
- revokeOAuthToken,
- fetchPinnedStatuses,
- pinOwnStatus,
- unpinOwnStatus,
- muteConversation,
- unmuteConversation,
- tagUser,
- untagUser,
- addRight,
- deleteRight,
- deleteUser,
- activateUser,
- deactivateUser,
- register,
- getCaptcha,
- updateAvatar,
- updateBg,
- updateBanner,
- updateProfile,
- importBlocks,
- importFollows,
- deleteAccount,
- changeEmail,
- changePassword,
- fetchSettingsMFA,
- generateMfaBackupCodes,
- mfaSetupOTP,
- mfaConfirmOTP,
- mfaDisableOTP,
- approveUser,
- denyUser,
- vote,
- fetchPoll,
- fetchFavoritedByUsers,
- fetchRebloggedByUsers,
- reportUser,
- favorite,
- unfavorite,
- retweet,
- unretweet,
- updateNotificationSettings,
- search2,
- searchUsers
- }
-
- return backendInteractorServiceInstance
-}
+ verifyCredentials: apiService.verifyCredentials
+})
export default backendInteractorService
diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js
index 598cb5f7..29b38a0f 100644
--- a/src/services/follow_manipulate/follow_manipulate.js
+++ b/src/services/follow_manipulate/follow_manipulate.js
@@ -39,7 +39,7 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => {
})
export const requestUnfollow = (user, store) => new Promise((resolve, reject) => {
- store.state.api.backendInteractor.unfollowUser(user.id)
+ store.state.api.backendInteractor.unfollowUser({ id: user.id })
.then((updated) => {
store.commit('updateUserRelationship', [updated])
resolve({