Merge branch 'streaming' into 'develop'

Streaming and Backend Interactor service overhaul, removed the need for copypasting

See merge request pleroma/pleroma-fe!1012
This commit is contained in:
HJ 2019-12-26 16:49:42 +00:00
commit b8f4b18ae5
22 changed files with 343 additions and 298 deletions

View file

@ -7,11 +7,11 @@ const FollowRequestCard = {
}, },
methods: { methods: {
approveUser () { 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) this.$store.dispatch('removeFollowRequest', this.user)
}, },
denyUser () { 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) this.$store.dispatch('removeFollowRequest', this.user)
} }
} }

View file

@ -45,12 +45,12 @@ const ModerationTools = {
toggleTag (tag) { toggleTag (tag) {
const store = this.$store const store = this.$store
if (this.tagsSet.has(tag)) { 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 } if (!response.ok) { return }
store.commit('untagUser', { user: this.user, tag }) store.commit('untagUser', { user: this.user, tag })
}) })
} else { } 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 } if (!response.ok) { return }
store.commit('tagUser', { user: this.user, tag }) store.commit('tagUser', { user: this.user, tag })
}) })
@ -59,19 +59,19 @@ const ModerationTools = {
toggleRight (right) { toggleRight (right) {
const store = this.$store const store = this.$store
if (this.user.rights[right]) { 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 } if (!response.ok) { return }
store.commit('updateRight', { user: this.user, right: right, value: false }) store.commit('updateRight', { user: this.user, right, value: false })
}) })
} else { } 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 } if (!response.ok) { return }
store.commit('updateRight', { user: this.user, right: right, value: true }) store.commit('updateRight', { user: this.user, right, value: true })
}) })
} }
}, },
toggleActivationStatus () { toggleActivationStatus () {
this.$store.dispatch('toggleActivationStatus', this.user) this.$store.dispatch('toggleActivationStatus', { user: this.user })
}, },
deleteUserDialog (show) { deleteUserDialog (show) {
this.showDeleteUserDialog = show this.showDeleteUserDialog = show
@ -80,7 +80,7 @@ const ModerationTools = {
const store = this.$store const store = this.$store
const user = this.user const user = this.user
const { id, name } = user const { id, name } = user
store.state.api.backendInteractor.deleteUser(user) store.state.api.backendInteractor.deleteUser({ user })
.then(e => { .then(e => {
this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id) this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id)
const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile' const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'

View file

@ -47,6 +47,11 @@ const Notifications = {
components: { components: {
Notification Notification
}, },
created () {
const { dispatch } = this.$store
dispatch('fetchAndUpdateNotifications')
},
watch: { watch: {
unseenCount (count) { unseenCount (count) {
if (count > 0) { if (count > 0) {

View file

@ -10,7 +10,7 @@ const PublicAndExternalTimeline = {
this.$store.dispatch('startFetchingTimeline', { timeline: 'publicAndExternal' }) this.$store.dispatch('startFetchingTimeline', { timeline: 'publicAndExternal' })
}, },
destroyed () { destroyed () {
this.$store.dispatch('stopFetching', 'publicAndExternal') this.$store.dispatch('stopFetchingTimeline', 'publicAndExternal')
} }
} }

View file

@ -10,7 +10,7 @@ const PublicTimeline = {
this.$store.dispatch('startFetchingTimeline', { timeline: 'public' }) this.$store.dispatch('startFetchingTimeline', { timeline: 'public' })
}, },
destroyed () { destroyed () {
this.$store.dispatch('stopFetching', 'public') this.$store.dispatch('stopFetchingTimeline', 'public')
} }
} }

View file

@ -84,7 +84,7 @@ const settings = {
} }
}]) }])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
// Special cases (need to transform values) // Special cases (need to transform values or perform actions first)
muteWordsString: { muteWordsString: {
get () { return this.$store.getters.mergedConfig.muteWords.join('\n') }, get () { return this.$store.getters.mergedConfig.muteWords.join('\n') },
set (value) { set (value) {
@ -93,6 +93,22 @@ const settings = {
value: filter(value.split('\n'), (word) => trim(word).length > 0) 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 // Updating nested properties

View file

@ -73,6 +73,15 @@
</li> </li>
</ul> </ul>
</li> </li>
<li>
<Checkbox v-model="useStreamingApi">
{{ $t('settings.useStreamingApi') }}
<br/>
<small>
{{ $t('settings.useStreamingApiWarning') }}
</small>
</Checkbox>
</li>
<li> <li>
<Checkbox v-model="autoLoad"> <Checkbox v-model="autoLoad">
{{ $t('settings.autoload') }} {{ $t('settings.autoload') }}

View file

@ -19,7 +19,7 @@ const TagTimeline = {
} }
}, },
destroyed () { destroyed () {
this.$store.dispatch('stopFetching', 'tag') this.$store.dispatch('stopFetchingTimeline', 'tag')
} }
} }

View file

@ -112,9 +112,9 @@ const UserProfile = {
} }
}, },
stopFetching () { stopFetching () {
this.$store.dispatch('stopFetching', 'user') this.$store.dispatch('stopFetchingTimeline', 'user')
this.$store.dispatch('stopFetching', 'favorites') this.$store.dispatch('stopFetchingTimeline', 'favorites')
this.$store.dispatch('stopFetching', 'media') this.$store.dispatch('stopFetchingTimeline', 'media')
}, },
switchUser (userNameOrId) { switchUser (userNameOrId) {
this.stopFetching() this.stopFetching()

View file

@ -64,7 +64,7 @@ const UserReportingModal = {
forward: this.forward, forward: this.forward,
statusIds: this.statusIdsToReport statusIds: this.statusIdsToReport
} }
this.$store.state.api.backendInteractor.reportUser(params) this.$store.state.api.backendInteractor.reportUser({ ...params })
.then(() => { .then(() => {
this.processing = false this.processing = false
this.resetState() this.resetState()

View file

@ -242,7 +242,7 @@ const UserSettings = {
}) })
}, },
importFollows (file) { importFollows (file) {
return this.$store.state.api.backendInteractor.importFollows(file) return this.$store.state.api.backendInteractor.importFollows({ file })
.then((status) => { .then((status) => {
if (!status) { if (!status) {
throw new Error('failed') throw new Error('failed')
@ -250,7 +250,7 @@ const UserSettings = {
}) })
}, },
importBlocks (file) { importBlocks (file) {
return this.$store.state.api.backendInteractor.importBlocks(file) return this.$store.state.api.backendInteractor.importBlocks({ file })
.then((status) => { .then((status) => {
if (!status) { if (!status) {
throw new Error('failed') throw new Error('failed')
@ -297,7 +297,7 @@ const UserSettings = {
newPassword: this.changePasswordInputs[1], newPassword: this.changePasswordInputs[1],
newPasswordConfirmation: this.changePasswordInputs[2] newPasswordConfirmation: this.changePasswordInputs[2]
} }
this.$store.state.api.backendInteractor.changePassword(params) this.$store.state.api.backendInteractor.changePassword({ params })
.then((res) => { .then((res) => {
if (res.status === 'success') { if (res.status === 'success') {
this.changedPassword = true this.changedPassword = true
@ -314,7 +314,7 @@ const UserSettings = {
email: this.newEmail, email: this.newEmail,
password: this.changeEmailPassword password: this.changeEmailPassword
} }
this.$store.state.api.backendInteractor.changeEmail(params) this.$store.state.api.backendInteractor.changeEmail({ params })
.then((res) => { .then((res) => {
if (res.status === 'success') { if (res.status === 'success') {
this.changedEmail = true this.changedEmail = true

View file

@ -358,6 +358,8 @@
"post_status_content_type": "Post status content type", "post_status_content_type": "Post status content type",
"stop_gifs": "Play-on-hover GIFs", "stop_gifs": "Play-on-hover GIFs",
"streaming": "Enable automatic streaming of new posts when scrolled to the top", "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", "text": "Text",
"theme": "Theme", "theme": "Theme",
"theme_help": "Use hex color codes (#rrggbb) to customize your color theme.", "theme_help": "Use hex color codes (#rrggbb) to customize your color theme.",

View file

@ -219,6 +219,8 @@
"subject_input_always_show": "Всегда показывать поле ввода темы", "subject_input_always_show": "Всегда показывать поле ввода темы",
"stop_gifs": "Проигрывать GIF анимации только при наведении", "stop_gifs": "Проигрывать GIF анимации только при наведении",
"streaming": "Включить автоматическую загрузку новых сообщений при прокрутке вверх", "streaming": "Включить автоматическую загрузку новых сообщений при прокрутке вверх",
"useStreamingApi": "Получать сообщения и уведомления в реальном времени",
"useStreamingApiWarning": "(Не рекомендуется, экспериментально, сообщения могут пропадать)",
"text": "Текст", "text": "Текст",
"theme": "Тема", "theme": "Тема",
"theme_help": "Используйте шестнадцатеричные коды цветов (#rrggbb) для настройки темы.", "theme_help": "Используйте шестнадцатеричные коды цветов (#rrggbb) для настройки темы.",

View file

@ -6,6 +6,7 @@ const api = {
backendInteractor: backendInteractorService(), backendInteractor: backendInteractorService(),
fetchers: {}, fetchers: {},
socket: null, socket: null,
mastoUserSocket: null,
followRequests: [] followRequests: []
}, },
mutations: { mutations: {
@ -15,7 +16,8 @@ const api = {
addFetcher (state, { fetcherName, fetcher }) { addFetcher (state, { fetcherName, fetcher }) {
state.fetchers[fetcherName] = fetcher state.fetchers[fetcherName] = fetcher
}, },
removeFetcher (state, { fetcherName }) { removeFetcher (state, { fetcherName, fetcher }) {
window.clearInterval(fetcher)
delete state.fetchers[fetcherName] delete state.fetchers[fetcherName]
}, },
setWsToken (state, token) { setWsToken (state, token) {
@ -29,32 +31,134 @@ const api = {
} }
}, },
actions: { actions: {
startFetchingTimeline (store, { timeline = 'friends', tag = false, userId = false }) { // Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets
// Don't start fetching if we already are. 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 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 }) store.commit('addFetcher', { fetcherName: timeline, fetcher })
}, },
startFetchingNotifications (store) { stopFetchingTimeline (store, timeline) {
// Don't start fetching if we already are. const fetcher = store.state.fetchers[timeline]
if (store.state.fetchers['notifications']) return 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 }) const fetcher = store.state.backendInteractor.startFetchingNotifications({ store })
store.commit('addFetcher', { fetcherName: 'notifications', fetcher }) store.commit('addFetcher', { fetcherName: 'notifications', fetcher })
}, },
startFetchingFollowRequest (store) { stopFetchingNotifications (store) {
// Don't start fetching if we already are. const fetcher = store.state.fetchers.notifications
if (store.state.fetchers['followRequest']) return if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
},
fetchAndUpdateNotifications (store) {
store.state.backendInteractor.fetchAndUpdateNotifications({ store })
},
const fetcher = store.state.backendInteractor.startFetchingFollowRequest({ store }) // Follow requests
store.commit('addFetcher', { fetcherName: 'followRequest', fetcher }) startFetchingFollowRequests (store) {
if (store.state.fetchers['followRequests']) return
const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
}, },
stopFetching (store, fetcherName) { stopFetchingFollowRequests (store) {
const fetcher = store.state.fetchers[fetcherName] const fetcher = store.state.fetchers.followRequests
window.clearInterval(fetcher) if (!fetcher) return
store.commit('removeFetcher', { fetcherName }) 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) { setWsToken (store, token) {
store.commit('setWsToken', token) store.commit('setWsToken', token)
}, },
@ -72,10 +176,6 @@ const api = {
disconnectFromSocket ({ commit, state }) { disconnectFromSocket ({ commit, state }) {
state.socket && state.socket.disconnect() state.socket && state.socket.disconnect()
commit('setSocket', null) commit('setSocket', null)
},
removeFollowRequest (store, request) {
let requests = store.state.followRequests.filter((it) => it !== request)
store.commit('setFollowRequests', requests)
} }
} }
} }

View file

@ -35,6 +35,7 @@ export const defaultState = {
highlight: {}, highlight: {},
interfaceLanguage: browserLocale, interfaceLanguage: browserLocale,
hideScopeNotice: false, hideScopeNotice: false,
useStreamingApi: false,
scopeCopy: undefined, // instance default scopeCopy: undefined, // instance default
subjectLineBehavior: undefined, // instance default subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined, // instance default alwaysShowSubjectInput: undefined, // instance default

View file

@ -9,7 +9,7 @@ const oauthTokens = {
}) })
}, },
revokeToken ({ rootState, commit, state }, id) { revokeToken ({ rootState, commit, state }, id) {
rootState.api.backendInteractor.revokeOAuthToken(id).then((response) => { rootState.api.backendInteractor.revokeOAuthToken({ id }).then((response) => {
if (response.status === 201) { if (response.status === 201) {
commit('swapTokens', state.tokens.filter(token => token.id !== id)) commit('swapTokens', state.tokens.filter(token => token.id !== id))
} }

View file

@ -40,7 +40,7 @@ const polls = {
commit('mergeOrAddPoll', poll) commit('mergeOrAddPoll', poll)
}, },
updateTrackedPoll ({ rootState, dispatch, commit }, pollId) { updateTrackedPoll ({ rootState, dispatch, commit }, pollId) {
rootState.api.backendInteractor.fetchPoll(pollId).then(poll => { rootState.api.backendInteractor.fetchPoll({ pollId }).then(poll => {
setTimeout(() => { setTimeout(() => {
if (rootState.polls.trackedPolls[pollId]) { if (rootState.polls.trackedPolls[pollId]) {
dispatch('updateTrackedPoll', pollId) dispatch('updateTrackedPoll', pollId)
@ -59,7 +59,7 @@ const polls = {
commit('untrackPoll', pollId) commit('untrackPoll', pollId)
}, },
votePoll ({ rootState, commit }, { id, pollId, choices }) { 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) commit('mergeOrAddPoll', poll)
return poll return poll
}) })

View file

@ -558,45 +558,45 @@ const statuses = {
favorite ({ rootState, commit }, status) { favorite ({ rootState, commit }, status) {
// Optimistic favoriting... // Optimistic favoriting...
commit('setFavorited', { status, value: true }) 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 })) .then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
}, },
unfavorite ({ rootState, commit }, status) { unfavorite ({ rootState, commit }, status) {
// Optimistic unfavoriting... // Optimistic unfavoriting...
commit('setFavorited', { status, value: false }) 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 })) .then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
}, },
fetchPinnedStatuses ({ rootState, dispatch }, userId) { 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 })) .then(statuses => dispatch('addNewStatuses', { statuses, timeline: 'user', userId, showImmediately: true, noIdUpdate: true }))
}, },
pinStatus ({ rootState, dispatch }, statusId) { pinStatus ({ rootState, dispatch }, statusId) {
return rootState.api.backendInteractor.pinOwnStatus(statusId) return rootState.api.backendInteractor.pinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] })) .then((status) => dispatch('addNewStatuses', { statuses: [status] }))
}, },
unpinStatus ({ rootState, dispatch }, statusId) { unpinStatus ({ rootState, dispatch }, statusId) {
rootState.api.backendInteractor.unpinOwnStatus(statusId) rootState.api.backendInteractor.unpinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] })) .then((status) => dispatch('addNewStatuses', { statuses: [status] }))
}, },
muteConversation ({ rootState, commit }, statusId) { muteConversation ({ rootState, commit }, statusId) {
return rootState.api.backendInteractor.muteConversation(statusId) return rootState.api.backendInteractor.muteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status)) .then((status) => commit('setMutedStatus', status))
}, },
unmuteConversation ({ rootState, commit }, statusId) { unmuteConversation ({ rootState, commit }, statusId) {
return rootState.api.backendInteractor.unmuteConversation(statusId) return rootState.api.backendInteractor.unmuteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status)) .then((status) => commit('setMutedStatus', status))
}, },
retweet ({ rootState, commit }, status) { retweet ({ rootState, commit }, status) {
// Optimistic retweeting... // Optimistic retweeting...
commit('setRetweeted', { status, value: true }) 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 })) .then(status => commit('setRetweetedConfirm', { status: status.retweeted_status, user: rootState.users.currentUser }))
}, },
unretweet ({ rootState, commit }, status) { unretweet ({ rootState, commit }, status) {
// Optimistic unretweeting... // Optimistic unretweeting...
commit('setRetweeted', { status, value: false }) 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 })) .then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser }))
}, },
queueFlush ({ rootState, commit }, { timeline, id }) { queueFlush ({ rootState, commit }, { timeline, id }) {
@ -611,19 +611,19 @@ const statuses = {
}, },
fetchFavsAndRepeats ({ rootState, commit }, id) { fetchFavsAndRepeats ({ rootState, commit }, id) {
Promise.all([ Promise.all([
rootState.api.backendInteractor.fetchFavoritedByUsers(id), rootState.api.backendInteractor.fetchFavoritedByUsers({ id }),
rootState.api.backendInteractor.fetchRebloggedByUsers(id) rootState.api.backendInteractor.fetchRebloggedByUsers({ id })
]).then(([favoritedByUsers, rebloggedByUsers]) => { ]).then(([favoritedByUsers, rebloggedByUsers]) => {
commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser }) commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser })
commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser }) commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser })
}) })
}, },
fetchFavs ({ rootState, commit }, id) { fetchFavs ({ rootState, commit }, id) {
rootState.api.backendInteractor.fetchFavoritedByUsers(id) rootState.api.backendInteractor.fetchFavoritedByUsers({ id })
.then(favoritedByUsers => commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser })) .then(favoritedByUsers => commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser }))
}, },
fetchRepeats ({ rootState, commit }, id) { fetchRepeats ({ rootState, commit }, id) {
rootState.api.backendInteractor.fetchRebloggedByUsers(id) rootState.api.backendInteractor.fetchRebloggedByUsers({ id })
.then(rebloggedByUsers => commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser })) .then(rebloggedByUsers => commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser }))
}, },
search (store, { q, resolve, limit, offset, following }) { search (store, { q, resolve, limit, offset, following }) {

View file

@ -32,7 +32,7 @@ const getNotificationPermission = () => {
} }
const blockUser = (store, id) => { const blockUser = (store, id) => {
return store.rootState.api.backendInteractor.blockUser(id) return store.rootState.api.backendInteractor.blockUser({ id })
.then((relationship) => { .then((relationship) => {
store.commit('updateUserRelationship', [relationship]) store.commit('updateUserRelationship', [relationship])
store.commit('addBlockId', id) store.commit('addBlockId', id)
@ -43,12 +43,12 @@ const blockUser = (store, id) => {
} }
const unblockUser = (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])) .then((relationship) => store.commit('updateUserRelationship', [relationship]))
} }
const muteUser = (store, id) => { const muteUser = (store, id) => {
return store.rootState.api.backendInteractor.muteUser(id) return store.rootState.api.backendInteractor.muteUser({ id })
.then((relationship) => { .then((relationship) => {
store.commit('updateUserRelationship', [relationship]) store.commit('updateUserRelationship', [relationship])
store.commit('addMuteId', id) store.commit('addMuteId', id)
@ -56,7 +56,7 @@ const muteUser = (store, id) => {
} }
const unmuteUser = (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])) .then((relationship) => store.commit('updateUserRelationship', [relationship]))
} }
@ -324,11 +324,11 @@ const users = {
commit('clearFollowers', userId) commit('clearFollowers', userId)
}, },
subscribeUser ({ rootState, commit }, id) { subscribeUser ({ rootState, commit }, id) {
return rootState.api.backendInteractor.subscribeUser(id) return rootState.api.backendInteractor.subscribeUser({ id })
.then((relationship) => commit('updateUserRelationship', [relationship])) .then((relationship) => commit('updateUserRelationship', [relationship]))
}, },
unsubscribeUser ({ rootState, commit }, id) { unsubscribeUser ({ rootState, commit }, id) {
return rootState.api.backendInteractor.unsubscribeUser(id) return rootState.api.backendInteractor.unsubscribeUser({ id })
.then((relationship) => commit('updateUserRelationship', [relationship])) .then((relationship) => commit('updateUserRelationship', [relationship]))
}, },
toggleActivationStatus ({ rootState, commit }, user) { toggleActivationStatus ({ rootState, commit }, user) {
@ -387,7 +387,7 @@ const users = {
}) })
}, },
searchUsers (store, query) { searchUsers (store, query) {
return store.rootState.api.backendInteractor.searchUsers(query) return store.rootState.api.backendInteractor.searchUsers({ query })
.then((users) => { .then((users) => {
store.commit('addNewUsers', users) store.commit('addNewUsers', users)
return users return users
@ -399,7 +399,7 @@ const users = {
let rootState = store.rootState let rootState = store.rootState
try { try {
let data = await rootState.api.backendInteractor.register(userInfo) let data = await rootState.api.backendInteractor.register({ ...userInfo })
store.commit('signUpSuccess') store.commit('signUpSuccess')
store.commit('setToken', data.access_token) store.commit('setToken', data.access_token)
store.dispatch('loginUser', data.access_token) store.dispatch('loginUser', data.access_token)
@ -436,10 +436,10 @@ const users = {
store.commit('clearCurrentUser') store.commit('clearCurrentUser')
store.dispatch('disconnectFromSocket') store.dispatch('disconnectFromSocket')
store.commit('clearToken') store.commit('clearToken')
store.dispatch('stopFetching', 'friends') store.dispatch('stopFetchingTimeline', 'friends')
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
store.dispatch('stopFetching', 'notifications') store.dispatch('stopFetchingNotifications')
store.dispatch('stopFetching', 'followRequest') store.dispatch('stopFetchingFollowRequests')
store.commit('clearNotifications') store.commit('clearNotifications')
store.commit('resetStatuses') store.commit('resetStatuses')
}) })
@ -474,11 +474,24 @@ const users = {
store.dispatch('initializeSocket') store.dispatch('initializeSocket')
} }
// Start getting fresh posts. const startPolling = () => {
store.dispatch('startFetchingTimeline', { timeline: 'friends' }) // Start getting fresh posts.
store.dispatch('startFetchingTimeline', { timeline: 'friends' })
// Start fetching notifications // Start fetching notifications
store.dispatch('startFetchingNotifications') 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 // Get user mutes
store.dispatch('fetchMutes') store.dispatch('fetchMutes')

View file

@ -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_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
const MASTODON_SEARCH_2 = `/api/v2/search` const MASTODON_SEARCH_2 = `/api/v2/search`
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search' const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
const MASTODON_STREAMING = '/api/v1/streaming'
const oldfetch = window.fetch 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({ return promisedRequest({
url: ACTIVATE_USER_URL, url: ACTIVATE_USER_URL,
method: 'PATCH', method: 'PATCH',
@ -462,7 +463,7 @@ const activateUser = ({ credentials, screen_name: nickname }) => {
}).then(response => get(response, 'users.0')) }).then(response => get(response, 'users.0'))
} }
const deactivateUser = ({ credentials, screen_name: nickname }) => { const deactivateUser = ({ credentials, user: { screen_name: nickname } }) => {
return promisedRequest({ return promisedRequest({
url: DEACTIVATE_USER_URL, url: DEACTIVATE_USER_URL,
method: 'PATCH', 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 = { const apiService = {
verifyCredentials, verifyCredentials,
fetchTimeline, fetchTimeline,

View file

@ -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 timelineFetcherService 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 => ({
const fetchStatus = ({ id }) => { startFetchingTimeline ({ timeline, store, userId = false, tag }) {
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 }) => {
return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag }) return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag })
} },
const startFetchingNotifications = ({ store }) => { startFetchingNotifications ({ store }) {
return notificationsFetcher.startFetching({ store, credentials }) return notificationsFetcher.startFetching({ store, credentials })
} },
const startFetchingFollowRequest = ({ store }) => { fetchAndUpdateNotifications ({ store }) {
return notificationsFetcher.fetchAndUpdate({ store, credentials })
},
startFetchingFollowRequest ({ store }) {
return followRequestFetcher.startFetching({ store, credentials }) return followRequestFetcher.startFetching({ store, credentials })
} },
// eslint-disable-next-line camelcase startUserSocket ({ store }) {
const tagUser = ({ screen_name }, tag) => { const serv = store.rootState.instance.server.replace('http', 'ws')
return apiService.tagUser({ screen_name, tag, credentials }) const url = serv + getMastodonSocketURI({ credentials, stream: 'user' })
} return ProcessedWS({ url, id: 'User' })
},
// eslint-disable-next-line camelcase ...Object.entries(apiService).reduce((acc, [key, func]) => {
const untagUser = ({ screen_name }, tag) => { return {
return apiService.untagUser({ screen_name, tag, credentials }) ...acc,
} [key]: (args) => func({ credentials, ...args })
}
}, {}),
// eslint-disable-next-line camelcase verifyCredentials: apiService.verifyCredentials
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
}
export default backendInteractorService export default backendInteractorService

View file

@ -39,7 +39,7 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => {
}) })
export const requestUnfollow = (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) => { .then((updated) => {
store.commit('updateUserRelationship', [updated]) store.commit('updateUserRelationship', [updated])
resolve({ resolve({