From ddb6fb9217789e90490a4ec1ce7a2dd9ced67631 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 24 Nov 2019 13:57:46 +0200 Subject: [PATCH 01/10] Backend Interactor service overhaul, removed the need for copypasting --- .../follow_request_card.js | 4 +- .../moderation_tools/moderation_tools.js | 16 +- .../user_reporting_modal.js | 2 +- src/components/user_settings/user_settings.js | 8 +- src/modules/oauth_tokens.js | 2 +- src/modules/polls.js | 4 +- src/modules/statuses.js | 26 +- src/modules/users.js | 16 +- .../backend_interactor_service.js | 231 ++---------------- .../follow_manipulate/follow_manipulate.js | 2 +- 10 files changed, 55 insertions(+), 256 deletions(-) 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 8aadc8c5..5bb76497 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,21 +59,21 @@ 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 () { const store = this.$store const status = !!this.user.deactivated - store.state.api.backendInteractor.setActivationStatus(this.user, status).then(response => { + store.state.api.backendInteractor.setActivationStatus({ user: this.user, status }).then(response => { if (!response.ok) { return } store.commit('updateActivationStatus', { user: this.user, status: status }) }) @@ -85,7 +85,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/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/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 f11ffdcd..6a743a4a 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -551,45 +551,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 }) { @@ -604,19 +604,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 14b2d8b5..e1373220 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])) }, registerPushNotifications (store) { @@ -382,7 +382,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 @@ -394,7 +394,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) diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index c16bd1f1..57fdccde 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -3,228 +3,27 @@ import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service 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 }) => { + startFetchingFollowRequest ({ store }) { return followRequestFetcher.startFetching({ store, credentials }) - } + }, - // eslint-disable-next-line camelcase - const tagUser = ({ screen_name }, tag) => { - return apiService.tagUser({ screen_name, tag, credentials }) - } + ...Object.entries(apiService).reduce((acc, [key, func]) => { + return { + ...acc, + [key]: (args) => func({ credentials, ...args }) + } + }, {}), - // eslint-disable-next-line camelcase - const untagUser = ({ screen_name }, tag) => { - return apiService.untagUser({ screen_name, tag, credentials }) - } - - // 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 setActivationStatus = ({ screen_name }, status) => { - return apiService.setActivationStatus({ screen_name, status, 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, - setActivationStatus, - 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({ From 319bb4ac2895b8eb62da42e3f95addc9bb67b1a0 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 24 Nov 2019 18:50:28 +0200 Subject: [PATCH 02/10] initial streaming work --- src/modules/api.js | 15 +++++++ src/modules/users.js | 11 +++-- src/services/api/api.service.js | 40 +++++++++++++++++++ .../backend_interactor_service.js | 15 ++++++- 4 files changed, 76 insertions(+), 5 deletions(-) diff --git a/src/modules/api.js b/src/modules/api.js index 1293e3c8..1bf65db5 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -6,6 +6,7 @@ const api = { backendInteractor: backendInteractorService(), fetchers: {}, socket: null, + mastoSocket: null, followRequests: [] }, mutations: { @@ -29,6 +30,20 @@ const api = { } }, actions: { + startMastoSocket (store) { + store.state.mastoSocket = store.state.backendInteractor + .startUserSocket({ + store, + onMessage: (message) => { + if (!message) return + if (message.event === 'notification') { + store.dispatch('addNewNotifications', { notifications: [message.notification], older: false }) + } else if (message.event === 'update') { + store.dispatch('addNewStatuses', { statuses: [message.status], userId: false, showImmediately: false, timeline: 'friends' }) + } + } + }) + }, startFetchingTimeline (store, { timeline = 'friends', tag = false, userId = false }) { // Don't start fetching if we already are. if (store.state.fetchers[timeline]) return diff --git a/src/modules/users.js b/src/modules/users.js index e1373220..861a2f4f 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -469,11 +469,14 @@ const users = { store.dispatch('initializeSocket') } - // Start getting fresh posts. - store.dispatch('startFetchingTimeline', { timeline: 'friends' }) + store.dispatch('startMastoSocket').catch((error) => { + console.error(error) + // Start getting fresh posts. + store.dispatch('startFetchingTimeline', { timeline: 'friends' }) - // Start fetching notifications - store.dispatch('startFetchingNotifications') + // Start fetching notifications + store.dispatch('startFetchingNotifications') + }) // Get user mutes store.dispatch('fetchMutes') diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 8f5eb416..7f27564f 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -71,6 +71,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 @@ -932,6 +933,45 @@ 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' +]) + +export const handleMastoWS = (wsEvent) => { + console.debug('Event', wsEvent) + const { data } = wsEvent + if (!data) return + const parsedEvent = JSON.parse(data) + const { event, payload } = parsedEvent + if (MASTODON_STREAMING_EVENTS.has(event)) { + 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 57fdccde..0cef4640 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -1,4 +1,4 @@ -import apiService from '../api/api.service.js' +import apiService, { getMastodonSocketURI, handleMastoWS } 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' @@ -16,6 +16,19 @@ const backendInteractorService = credentials => ({ return followRequestFetcher.startFetching({ store, credentials }) }, + startUserSocket ({ store, onMessage }) { + const serv = store.rootState.instance.server.replace('https', 'wss') + // const serb = 'ws://localhost:8080/' + const url = serv + getMastodonSocketURI({ credentials, stream: 'user' }) + const socket = new WebSocket(url) + console.log(socket) + if (socket) { + socket.addEventListener('message', (wsEvent) => onMessage(handleMastoWS(wsEvent))) + } else { + throw new Error('failed to connect to socket') + } + }, + ...Object.entries(apiService).reduce((acc, [key, func]) => { return { ...acc, From 172ebaf4e67358852bfaafd8f069763ca5e602b1 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 24 Nov 2019 22:01:12 +0200 Subject: [PATCH 03/10] improved initial notifications fetching --- src/components/notifications/notifications.js | 5 ++++ src/modules/api.js | 23 ++++++++++++++++--- src/modules/users.js | 2 +- .../backend_interactor_service.js | 9 ++++++-- 4 files changed, 33 insertions(+), 6 deletions(-) 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/modules/api.js b/src/modules/api.js index 1bf65db5..0e7e5e19 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -31,18 +31,32 @@ const api = { }, actions: { startMastoSocket (store) { - store.state.mastoSocket = store.state.backendInteractor + const { state, dispatch } = store + state.mastoSocket = state.backendInteractor .startUserSocket({ store, onMessage: (message) => { if (!message) return if (message.event === 'notification') { - store.dispatch('addNewNotifications', { notifications: [message.notification], older: false }) + dispatch('addNewNotifications', { + notifications: [message.notification], + older: false + }) } else if (message.event === 'update') { - store.dispatch('addNewStatuses', { statuses: [message.status], userId: false, showImmediately: false, timeline: 'friends' }) + dispatch('addNewStatuses', { + statuses: [message.status], + userId: false, + showImmediately: false, + timeline: 'friends' + }) } } }) + state.mastoSocket.addEventListener('error', error => { + console.error('Error with MastoAPI websocket:', error) + dispatch('startFetchingTimeline', { timeline: 'friends' }) + dispatch('startFetchingNotifications') + }) }, startFetchingTimeline (store, { timeline = 'friends', tag = false, userId = false }) { // Don't start fetching if we already are. @@ -58,6 +72,9 @@ const api = { const fetcher = store.state.backendInteractor.startFetchingNotifications({ store }) store.commit('addFetcher', { fetcherName: 'notifications', fetcher }) }, + fetchAndUpdateNotifications (store) { + store.state.backendInteractor.fetchAndUpdateNotifications({ store }) + }, startFetchingFollowRequest (store) { // Don't start fetching if we already are. if (store.state.fetchers['followRequest']) return diff --git a/src/modules/users.js b/src/modules/users.js index 861a2f4f..eff0c5d5 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -470,7 +470,7 @@ const users = { } store.dispatch('startMastoSocket').catch((error) => { - console.error(error) + console.error('Failed initializing MastoAPI Streaming socket', error) // Start getting fresh posts. store.dispatch('startFetchingTimeline', { timeline: 'friends' }) diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 0cef4640..850b7867 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -12,18 +12,23 @@ const backendInteractorService = credentials => ({ return notificationsFetcher.startFetching({ store, credentials }) }, + fetchAndUpdateNotifications ({ store }) { + return notificationsFetcher.fetchAndUpdate({ store, credentials }) + }, + startFetchingFollowRequest ({ store }) { return followRequestFetcher.startFetching({ store, credentials }) }, startUserSocket ({ store, onMessage }) { - const serv = store.rootState.instance.server.replace('https', 'wss') - // const serb = 'ws://localhost:8080/' + const serv = store.rootState.instance.server.replace('http', 'ws') const url = serv + getMastodonSocketURI({ credentials, stream: 'user' }) const socket = new WebSocket(url) console.log(socket) if (socket) { socket.addEventListener('message', (wsEvent) => onMessage(handleMastoWS(wsEvent))) + socket.addEventListener('error', (error) => console.error('WebSocket Error:', error)) + return socket } else { throw new Error('failed to connect to socket') } From ff95d865d223fed5bab2a4d72ff3a22f014d3c56 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 8 Dec 2019 16:05:41 +0200 Subject: [PATCH 04/10] Updated streaming and improved error-handling, some more refactoring to api --- .../public_and_external_timeline.js | 2 +- .../public_timeline/public_timeline.js | 2 +- src/components/tag_timeline/tag_timeline.js | 2 +- src/components/user_profile/user_profile.js | 6 +- src/modules/api.js | 99 +++++++++++++------ src/modules/users.js | 8 +- .../backend_interactor_service.js | 5 +- 7 files changed, 84 insertions(+), 40 deletions(-) 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/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/modules/api.js b/src/modules/api.js index 0e7e5e19..185c9db6 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -6,7 +6,7 @@ const api = { backendInteractor: backendInteractorService(), fetchers: {}, socket: null, - mastoSocket: null, + mastoUserSocket: null, followRequests: [] }, mutations: { @@ -16,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) { @@ -30,13 +31,14 @@ const api = { } }, actions: { - startMastoSocket (store) { + // MastoAPI 'User' sockets + startMastoUserSocket (store) { const { state, dispatch } = store - state.mastoSocket = state.backendInteractor + state.mastoUserSocket = state.backendInteractor .startUserSocket({ store, onMessage: (message) => { - if (!message) return + if (!message) return // pings if (message.event === 'notification') { dispatch('addNewNotifications', { notifications: [message.notification], @@ -52,41 +54,86 @@ const api = { } } }) - state.mastoSocket.addEventListener('error', error => { - console.error('Error with MastoAPI websocket:', error) - dispatch('startFetchingTimeline', { timeline: 'friends' }) - dispatch('startFetchingNotifications') + state.mastoUserSocket.addEventListener('error', error => { + console.error('Error in MastoAPI websocket:', error) + }) + state.mastoUserSocket.addEventListener('close', closeEvent => { + const ignoreCodes = new Set([ + 1000, // Normal (intended) closure + 1001 // Going away + ]) + const { code } = closeEvent + console.debug('Socket closure event:', 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') + } }) }, - startFetchingTimeline (store, { timeline = 'friends', tag = false, userId = false }) { - // Don't start fetching if we already are. + restartMastoUserSocket ({ dispatch }) { + // This basically starts MastoAPI user socket and stops conventional + // fetchers when connection reestablished + dispatch('startMastoUserSocket').then(() => { + dispatch('stopFetchingTimeline', { timeline: 'friends' }) + dispatch('stopFetchingNotifications') + }) + }, + + // 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 }) }, + stopFetchingNotifications (store) { + const fetcher = store.state.fetchers.notifications + if (!fetcher) return + store.commit('removeFetcher', { fetcherName: 'notifications', fetcher }) + }, fetchAndUpdateNotifications (store) { store.state.backendInteractor.fetchAndUpdateNotifications({ store }) }, - startFetchingFollowRequest (store) { - // Don't start fetching if we already are. - if (store.state.fetchers['followRequest']) return - 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) }, @@ -104,10 +151,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/users.js b/src/modules/users.js index eff0c5d5..6bdaf193 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -431,10 +431,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') }) @@ -469,7 +469,7 @@ const users = { store.dispatch('initializeSocket') } - store.dispatch('startMastoSocket').catch((error) => { + store.dispatch('startMastoUserSocket').catch((error) => { console.error('Failed initializing MastoAPI Streaming socket', error) // Start getting fresh posts. store.dispatch('startFetchingTimeline', { timeline: 'friends' }) diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 850b7867..33b79a40 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -24,10 +24,11 @@ const backendInteractorService = credentials => ({ const serv = store.rootState.instance.server.replace('http', 'ws') const url = serv + getMastodonSocketURI({ credentials, stream: 'user' }) const socket = new WebSocket(url) - console.log(socket) + console.debug('Socket created:', socket) if (socket) { + socket.addEventListener('open', (wsEvent) => console.debug('MastoAPI User WebSocket connection established')) socket.addEventListener('message', (wsEvent) => onMessage(handleMastoWS(wsEvent))) - socket.addEventListener('error', (error) => console.error('WebSocket Error:', error)) + socket.addEventListener('error', (error) => console.error('MastoApi User WebSocket Error:', error)) return socket } else { throw new Error('failed to connect to socket') From 505fb260610e557e27bbc5d27515337ea07e0e3e Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 8 Dec 2019 19:18:38 +0200 Subject: [PATCH 05/10] better wrapper for websocket --- src/modules/api.js | 43 +++++++++-------- src/services/api/api.service.js | 46 ++++++++++++++++++- .../backend_interactor_service.js | 15 ++---- 3 files changed, 69 insertions(+), 35 deletions(-) diff --git a/src/modules/api.js b/src/modules/api.js index 185c9db6..593f8498 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -34,36 +34,35 @@ const api = { // MastoAPI 'User' sockets startMastoUserSocket (store) { const { state, dispatch } = store - state.mastoUserSocket = state.backendInteractor - .startUserSocket({ - store, - onMessage: (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: false, - timeline: '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: false, + timeline: 'friends' + }) } - }) - state.mastoUserSocket.addEventListener('error', error => { + } + ) + state.mastoUserSocket.addEventListener('error', ({ detail: error }) => { console.error('Error in MastoAPI websocket:', error) }) - state.mastoUserSocket.addEventListener('close', closeEvent => { + state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => { const ignoreCodes = new Set([ 1000, // Normal (intended) closure 1001 // Going away ]) const { code } = closeEvent - console.debug('Socket closure event:', closeEvent) if (ignoreCodes.has(code)) { console.debug(`Not restarting socket becasue of closure code ${code} is in ignore list`) } else { diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 7f27564f..c6818df4 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -953,8 +953,52 @@ const MASTODON_STREAMING_EVENTS = new Set([ '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 + ) + }) + socket.addEventListener('message', (wsEvent) => { + console.debug( + `[WS][${id}] Message received`, + wsEvent + ) + }) + + proxy(socket, 'open') + proxy(socket, 'close') + proxy(socket, 'message', preprocessor) + proxy(socket, 'error') + + return eventTarget +} + export const handleMastoWS = (wsEvent) => { - console.debug('Event', wsEvent) const { data } = wsEvent if (!data) return const parsedEvent = JSON.parse(data) diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 33b79a40..b7372ed0 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -1,4 +1,4 @@ -import apiService, { getMastodonSocketURI, handleMastoWS } 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' @@ -20,19 +20,10 @@ const backendInteractorService = credentials => ({ return followRequestFetcher.startFetching({ store, credentials }) }, - startUserSocket ({ store, onMessage }) { + startUserSocket ({ store }) { const serv = store.rootState.instance.server.replace('http', 'ws') const url = serv + getMastodonSocketURI({ credentials, stream: 'user' }) - const socket = new WebSocket(url) - console.debug('Socket created:', socket) - if (socket) { - socket.addEventListener('open', (wsEvent) => console.debug('MastoAPI User WebSocket connection established')) - socket.addEventListener('message', (wsEvent) => onMessage(handleMastoWS(wsEvent))) - socket.addEventListener('error', (error) => console.error('MastoApi User WebSocket Error:', error)) - return socket - } else { - throw new Error('failed to connect to socket') - } + return ProcessedWS({ url, id: 'User' }) }, ...Object.entries(apiService).reduce((acc, [key, func]) => { From 6acd889589e46b18491d96b5fa992154b4e58d88 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 10 Dec 2019 21:30:27 +0200 Subject: [PATCH 06/10] Option to enable streaming --- src/components/settings/settings.js | 14 +++++++++++++- src/components/settings/settings.vue | 9 +++++++++ src/i18n/en.json | 2 ++ src/i18n/ru.json | 2 ++ src/modules/api.js | 18 ++++++++++++++++++ src/modules/config.js | 1 + src/modules/users.js | 14 +++++++++++--- src/services/api/api.service.js | 6 ++++++ 8 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index c49083f9..2d7723cc 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,18 @@ 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 }) + }) + } } }, // 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/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 593f8498..dc91d00e 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -31,6 +31,18 @@ const api = { } }, 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 + dispatch('startMastoUserSocket') + }, + disableMastoSockets (store) { + const { state, dispatch } = store + if (!state.mastoUserSocket) return + dispatch('stopMastoUserSocket') + }, + // MastoAPI 'User' sockets startMastoUserSocket (store) { const { state, dispatch } = store @@ -81,6 +93,12 @@ const api = { dispatch('stopFetchingNotifications') }) }, + stopMastoUserSocket ({ state, dispatch }) { + dispatch('startFetchingTimeline', { timeline: 'friends' }) + dispatch('startFetchingNotifications') + console.log(state.mastoUserSocket) + state.mastoUserSocket.close() + }, // Timelines startFetchingTimeline (store, { 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/users.js b/src/modules/users.js index 6bdaf193..cbec6063 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -469,14 +469,22 @@ const users = { store.dispatch('initializeSocket') } - store.dispatch('startMastoUserSocket').catch((error) => { - console.error('Failed initializing MastoAPI Streaming socket', error) + const startPolling = () => { // Start getting fresh posts. store.dispatch('startFetchingTimeline', { timeline: 'friends' }) // 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() + }) + } 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 c6818df4..517b953e 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -983,18 +983,24 @@ export const ProcessedWS = ({ 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 } From 63a5f50e7c4acfc7676a1093990d0377dcb1a39f Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 11 Dec 2019 18:20:43 +0200 Subject: [PATCH 07/10] fix deletes causing errors --- src/services/api/api.service.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 517b953e..5f706dc0 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1010,6 +1010,10 @@ export const handleMastoWS = (wsEvent) => { 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) } From 585702b1cedf25547ff5faf0170263088e5484a6 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 12 Dec 2019 18:53:36 +0200 Subject: [PATCH 08/10] fix desktop notifications not working with streaming --- src/modules/users.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/users.js b/src/modules/users.js index cbec6063..5dc2f36f 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -481,6 +481,8 @@ const users = { store.dispatch('enableMastoSockets').catch((error) => { console.error('Failed initializing MastoAPI Streaming socket', error) startPolling() + }).then(() => { + setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000) }) } else { startPolling() From 43197c424366099301e59d3d1c7be58b987cb833 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 26 Dec 2019 14:12:35 +0200 Subject: [PATCH 09/10] Some error handling --- src/components/settings/settings.js | 4 ++ src/modules/api.js | 87 ++++++++++++++++------------- 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index 2d7723cc..31a9e9be 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -103,6 +103,10 @@ const settings = { 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 }) }) } } diff --git a/src/modules/api.js b/src/modules/api.js index dc91d00e..b6dd7fcf 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -35,60 +35,67 @@ const api = { enableMastoSockets (store) { const { state, dispatch } = store if (state.mastoUserSocket) return - dispatch('startMastoUserSocket') + return dispatch('startMastoUserSocket') }, disableMastoSockets (store) { const { state, dispatch } = store if (!state.mastoUserSocket) return - dispatch('stopMastoUserSocket') + return dispatch('stopMastoUserSocket') }, // MastoAPI 'User' sockets startMastoUserSocket (store) { - const { state, dispatch } = store - 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: false, - 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') + return new Promise((resolve, reject) => { + try { + const { state, dispatch } = store + 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: false, + 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 - dispatch('startMastoUserSocket').then(() => { + return dispatch('startMastoUserSocket').then(() => { dispatch('stopFetchingTimeline', { timeline: 'friends' }) dispatch('stopFetchingNotifications') }) From bd07e5de1c03a5619db5af49f14dc20602134881 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 26 Dec 2019 14:35:46 +0200 Subject: [PATCH 10/10] unify showimmideately --- src/modules/api.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/api.js b/src/modules/api.js index b6dd7fcf..9c296275 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -47,7 +47,8 @@ const api = { startMastoUserSocket (store) { return new Promise((resolve, reject) => { try { - const { state, dispatch } = store + const { state, dispatch, rootState } = store + const timelineData = rootState.statuses.timelines.friends state.mastoUserSocket = state.backendInteractor.startUserSocket({ store }) state.mastoUserSocket.addEventListener( 'message', @@ -62,7 +63,7 @@ const api = { dispatch('addNewStatuses', { statuses: [message.status], userId: false, - showImmediately: false, + showImmediately: timelineData.visibleStatuses.length === 0, timeline: 'friends' }) }