0) {
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 71876b1451..148ac7f242 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -76,7 +76,7 @@
}
}
- .follow-text {
+ .follow-text, .move-text {
padding: 0.5em 0;
}
@@ -151,6 +151,11 @@
color: var(--cOrange, $fallback--cOrange);
}
+ .icon-arrow-curved.lit {
+ color: $fallback--cBlue;
+ color: var(--cBlue, $fallback--cBlue);
+ }
+
.status-content {
margin: 0;
max-height: 300px;
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 f614c13b06..cbd4491bd6 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 8976a99c4a..66c40d3ab2 100644
--- a/src/components/public_timeline/public_timeline.js
+++ b/src/components/public_timeline/public_timeline.js
@@ -10,7 +10,7 @@ const PublicTimeline = {
this.$store.dispatch('startFetchingTimeline', { timeline: 'public' })
},
destroyed () {
- this.$store.dispatch('stopFetching', 'public')
+ this.$store.dispatch('stopFetchingTimeline', 'public')
}
}
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index c49083f9fc..31a9e9be79 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -84,7 +84,7 @@ const settings = {
}
}])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
- // Special cases (need to transform values)
+ // Special cases (need to transform values or perform actions first)
muteWordsString: {
get () { return this.$store.getters.mergedConfig.muteWords.join('\n') },
set (value) {
@@ -93,6 +93,22 @@ const settings = {
value: filter(value.split('\n'), (word) => trim(word).length > 0)
})
}
+ },
+ useStreamingApi: {
+ get () { return this.$store.getters.mergedConfig.useStreamingApi },
+ set (value) {
+ const promise = value
+ ? this.$store.dispatch('enableMastoSockets')
+ : this.$store.dispatch('disableMastoSockets')
+
+ promise.then(() => {
+ this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
+ }).catch((e) => {
+ console.error('Failed starting MastoAPI Streaming socket', e)
+ this.$store.dispatch('disableMastoSockets')
+ this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
+ })
+ }
}
},
// Updating nested properties
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index c4021137e7..cef492f350 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') }}
@@ -314,6 +323,11 @@
{{ $t('settings.notification_visibility_mentions') }}
+
+
+ {{ $t('settings.notification_visibility_moves') }}
+
+
diff --git a/src/components/tag_timeline/tag_timeline.js b/src/components/tag_timeline/tag_timeline.js
index 458eb1c51c..400c6a4b2c 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 0005570733..9558a0bd20 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -112,9 +112,9 @@ const UserProfile = {
}
},
stopFetching () {
- this.$store.dispatch('stopFetching', 'user')
- this.$store.dispatch('stopFetching', 'favorites')
- this.$store.dispatch('stopFetching', 'media')
+ this.$store.dispatch('stopFetchingTimeline', 'user')
+ this.$store.dispatch('stopFetchingTimeline', 'favorites')
+ this.$store.dispatch('stopFetchingTimeline', 'media')
},
switchUser (userNameOrId) {
this.stopFetching()
diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js
index 833fa98a28..38cf117bf0 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/mfa.js b/src/components/user_settings/mfa.js
index 3090138a22..abf37062fb 100644
--- a/src/components/user_settings/mfa.js
+++ b/src/components/user_settings/mfa.js
@@ -139,7 +139,7 @@ const Mfa = {
// fetch settings from server
async fetchSettings () {
- let result = await this.backendInteractor.fetchSettingsMFA()
+ let result = await this.backendInteractor.settingsMFA()
if (result.error) return
this.settings = result.settings
this.settings.available = true
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 3fdc534037..d5d671e4e0 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -242,7 +242,7 @@ const UserSettings = {
})
},
importFollows (file) {
- return this.$store.state.api.backendInteractor.importFollows(file)
+ return this.$store.state.api.backendInteractor.importFollows({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
@@ -250,7 +250,7 @@ const UserSettings = {
})
},
importBlocks (file) {
- return this.$store.state.api.backendInteractor.importBlocks(file)
+ return this.$store.state.api.backendInteractor.importBlocks({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
@@ -297,7 +297,7 @@ const UserSettings = {
newPassword: this.changePasswordInputs[1],
newPasswordConfirmation: this.changePasswordInputs[2]
}
- this.$store.state.api.backendInteractor.changePassword(params)
+ this.$store.state.api.backendInteractor.changePassword({ params })
.then((res) => {
if (res.status === 'success') {
this.changedPassword = true
@@ -314,7 +314,7 @@ const UserSettings = {
email: this.newEmail,
password: this.changeEmailPassword
}
- this.$store.state.api.backendInteractor.changeEmail(params)
+ this.$store.state.api.backendInteractor.changeEmail({ params })
.then((res) => {
if (res.status === 'success') {
this.changedEmail = true
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 85146ef577..75d66b9ff5 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -110,7 +110,8 @@
"notifications": "Notifications",
"read": "Read!",
"repeated_you": "repeated your status",
- "no_more_notifications": "No more notifications"
+ "no_more_notifications": "No more notifications",
+ "migrated_to": "migrated to"
},
"polls": {
"add_poll": "Add Poll",
@@ -140,6 +141,7 @@
"interactions": {
"favs_repeats": "Repeats and Favorites",
"follows": "New follows",
+ "moves": "User migrates",
"load_older": "Load older interactions"
},
"post_status": {
@@ -311,6 +313,7 @@
"notification_visibility_likes": "Likes",
"notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats",
+ "notification_visibility_moves": "User Migrates",
"no_rich_text_description": "Strip rich text formatting from all posts",
"no_blocks": "No blocks",
"no_mutes": "No mutes",
@@ -358,6 +361,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 19e10f1e96..4cb2d49738 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 1293e3c841..9c29627533 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -6,6 +6,7 @@ const api = {
backendInteractor: backendInteractorService(),
fetchers: {},
socket: null,
+ mastoUserSocket: null,
followRequests: []
},
mutations: {
@@ -15,7 +16,8 @@ const api = {
addFetcher (state, { fetcherName, fetcher }) {
state.fetchers[fetcherName] = fetcher
},
- removeFetcher (state, { fetcherName }) {
+ removeFetcher (state, { fetcherName, fetcher }) {
+ window.clearInterval(fetcher)
delete state.fetchers[fetcherName]
},
setWsToken (state, token) {
@@ -29,32 +31,134 @@ const api = {
}
},
actions: {
- startFetchingTimeline (store, { timeline = 'friends', tag = false, userId = false }) {
- // Don't start fetching if we already are.
+ // Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets
+ enableMastoSockets (store) {
+ const { state, dispatch } = store
+ if (state.mastoUserSocket) return
+ return dispatch('startMastoUserSocket')
+ },
+ disableMastoSockets (store) {
+ const { state, dispatch } = store
+ if (!state.mastoUserSocket) return
+ return dispatch('stopMastoUserSocket')
+ },
+
+ // MastoAPI 'User' sockets
+ startMastoUserSocket (store) {
+ return new Promise((resolve, reject) => {
+ try {
+ const { state, dispatch, rootState } = store
+ const timelineData = rootState.statuses.timelines.friends
+ state.mastoUserSocket = state.backendInteractor.startUserSocket({ store })
+ state.mastoUserSocket.addEventListener(
+ 'message',
+ ({ detail: message }) => {
+ if (!message) return // pings
+ if (message.event === 'notification') {
+ dispatch('addNewNotifications', {
+ notifications: [message.notification],
+ older: false
+ })
+ } else if (message.event === 'update') {
+ dispatch('addNewStatuses', {
+ statuses: [message.status],
+ userId: false,
+ showImmediately: timelineData.visibleStatuses.length === 0,
+ timeline: 'friends'
+ })
+ }
+ }
+ )
+ state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
+ console.error('Error in MastoAPI websocket:', error)
+ })
+ state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
+ const ignoreCodes = new Set([
+ 1000, // Normal (intended) closure
+ 1001 // Going away
+ ])
+ const { code } = closeEvent
+ if (ignoreCodes.has(code)) {
+ console.debug(`Not restarting socket becasue of closure code ${code} is in ignore list`)
+ } else {
+ console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
+ dispatch('startFetchingTimeline', { timeline: 'friends' })
+ dispatch('startFetchingNotifications')
+ dispatch('restartMastoUserSocket')
+ }
+ })
+ resolve()
+ } catch (e) {
+ reject(e)
+ }
+ })
+ },
+ restartMastoUserSocket ({ dispatch }) {
+ // This basically starts MastoAPI user socket and stops conventional
+ // fetchers when connection reestablished
+ return dispatch('startMastoUserSocket').then(() => {
+ dispatch('stopFetchingTimeline', { timeline: 'friends' })
+ dispatch('stopFetchingNotifications')
+ })
+ },
+ stopMastoUserSocket ({ state, dispatch }) {
+ dispatch('startFetchingTimeline', { timeline: 'friends' })
+ dispatch('startFetchingNotifications')
+ console.log(state.mastoUserSocket)
+ state.mastoUserSocket.close()
+ },
+
+ // Timelines
+ startFetchingTimeline (store, {
+ timeline = 'friends',
+ tag = false,
+ userId = false
+ }) {
if (store.state.fetchers[timeline]) return
- const fetcher = store.state.backendInteractor.startFetchingTimeline({ timeline, store, userId, tag })
+ const fetcher = store.state.backendInteractor.startFetchingTimeline({
+ timeline, store, userId, tag
+ })
store.commit('addFetcher', { fetcherName: timeline, fetcher })
},
- startFetchingNotifications (store) {
- // Don't start fetching if we already are.
- if (store.state.fetchers['notifications']) return
+ stopFetchingTimeline (store, timeline) {
+ const fetcher = store.state.fetchers[timeline]
+ if (!fetcher) return
+ store.commit('removeFetcher', { fetcherName: timeline, fetcher })
+ },
+ // Notifications
+ startFetchingNotifications (store) {
+ if (store.state.fetchers.notifications) return
const fetcher = store.state.backendInteractor.startFetchingNotifications({ store })
store.commit('addFetcher', { fetcherName: 'notifications', fetcher })
},
- startFetchingFollowRequest (store) {
- // Don't start fetching if we already are.
- if (store.state.fetchers['followRequest']) return
+ stopFetchingNotifications (store) {
+ const fetcher = store.state.fetchers.notifications
+ if (!fetcher) return
+ store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
+ },
+ fetchAndUpdateNotifications (store) {
+ store.state.backendInteractor.fetchAndUpdateNotifications({ store })
+ },
- const fetcher = store.state.backendInteractor.startFetchingFollowRequest({ store })
- store.commit('addFetcher', { fetcherName: 'followRequest', fetcher })
+ // Follow requests
+ startFetchingFollowRequests (store) {
+ if (store.state.fetchers['followRequests']) return
+ const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
+ store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
},
- stopFetching (store, fetcherName) {
- const fetcher = store.state.fetchers[fetcherName]
- window.clearInterval(fetcher)
- store.commit('removeFetcher', { fetcherName })
+ stopFetchingFollowRequests (store) {
+ const fetcher = store.state.fetchers.followRequests
+ if (!fetcher) return
+ store.commit('removeFetcher', { fetcherName: 'followRequests', fetcher })
},
+ removeFollowRequest (store, request) {
+ let requests = store.state.followRequests.filter((it) => it !== request)
+ store.commit('setFollowRequests', requests)
+ },
+
+ // Pleroma websocket
setWsToken (store, token) {
store.commit('setWsToken', token)
},
@@ -72,10 +176,6 @@ const api = {
disconnectFromSocket ({ commit, state }) {
state.socket && state.socket.disconnect()
commit('setSocket', null)
- },
- removeFollowRequest (store, request) {
- let requests = store.state.followRequests.filter((it) => it !== request)
- store.commit('setFollowRequests', requests)
}
}
}
diff --git a/src/modules/config.js b/src/modules/config.js
index 329b4091cb..de9f041b0d 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -28,13 +28,15 @@ export const defaultState = {
follows: true,
mentions: true,
likes: true,
- repeats: true
+ repeats: true,
+ moves: true
},
webPushNotifications: false,
muteWords: [],
highlight: {},
interfaceLanguage: browserLocale,
hideScopeNotice: false,
+ useStreamingApi: false,
scopeCopy: undefined, // instance default
subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined, // instance default
diff --git a/src/modules/oauth_tokens.js b/src/modules/oauth_tokens.js
index 0159a3f11a..907cae4ac9 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 e6158b63f3..92b89a06eb 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 e3a1f29341..16dae8ce16 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -67,7 +67,8 @@ const visibleNotificationTypes = (rootState) => {
rootState.config.notificationVisibility.likes && 'like',
rootState.config.notificationVisibility.mentions && 'mention',
rootState.config.notificationVisibility.repeats && 'repeat',
- rootState.config.notificationVisibility.follows && 'follow'
+ rootState.config.notificationVisibility.follows && 'follow',
+ rootState.config.notificationVisibility.moves && 'move'
].filter(_ => _)
}
@@ -306,7 +307,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
each(notifications, (notification) => {
- if (notification.type !== 'follow') {
+ if (notification.type !== 'follow' && notification.type !== 'move') {
notification.action = addStatusToGlobalStorage(state, notification.action).item
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
}
@@ -339,6 +340,9 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
case 'follow':
i18nString = 'followed_you'
break
+ case 'move':
+ i18nString = 'migrated_to'
+ break
}
if (i18nString) {
@@ -558,45 +562,45 @@ const statuses = {
favorite ({ rootState, commit }, status) {
// Optimistic favoriting...
commit('setFavorited', { status, value: true })
- rootState.api.backendInteractor.favorite(status.id)
+ rootState.api.backendInteractor.favorite({ id: status.id })
.then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
},
unfavorite ({ rootState, commit }, status) {
// Optimistic unfavoriting...
commit('setFavorited', { status, value: false })
- rootState.api.backendInteractor.unfavorite(status.id)
+ rootState.api.backendInteractor.unfavorite({ id: status.id })
.then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
},
fetchPinnedStatuses ({ rootState, dispatch }, userId) {
- rootState.api.backendInteractor.fetchPinnedStatuses(userId)
+ rootState.api.backendInteractor.fetchPinnedStatuses({ id: userId })
.then(statuses => dispatch('addNewStatuses', { statuses, timeline: 'user', userId, showImmediately: true, noIdUpdate: true }))
},
pinStatus ({ rootState, dispatch }, statusId) {
- return rootState.api.backendInteractor.pinOwnStatus(statusId)
+ return rootState.api.backendInteractor.pinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
unpinStatus ({ rootState, dispatch }, statusId) {
- rootState.api.backendInteractor.unpinOwnStatus(statusId)
+ rootState.api.backendInteractor.unpinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
muteConversation ({ rootState, commit }, statusId) {
- return rootState.api.backendInteractor.muteConversation(statusId)
+ return rootState.api.backendInteractor.muteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status))
},
unmuteConversation ({ rootState, commit }, statusId) {
- return rootState.api.backendInteractor.unmuteConversation(statusId)
+ return rootState.api.backendInteractor.unmuteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status))
},
retweet ({ rootState, commit }, status) {
// Optimistic retweeting...
commit('setRetweeted', { status, value: true })
- rootState.api.backendInteractor.retweet(status.id)
+ rootState.api.backendInteractor.retweet({ id: status.id })
.then(status => commit('setRetweetedConfirm', { status: status.retweeted_status, user: rootState.users.currentUser }))
},
unretweet ({ rootState, commit }, status) {
// Optimistic unretweeting...
commit('setRetweeted', { status, value: false })
- rootState.api.backendInteractor.unretweet(status.id)
+ rootState.api.backendInteractor.unretweet({ id: status.id })
.then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser }))
},
queueFlush ({ rootState, commit }, { timeline, id }) {
@@ -611,19 +615,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 82d3c4e8f0..84fa025552 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -32,7 +32,7 @@ const getNotificationPermission = () => {
}
const blockUser = (store, id) => {
- return store.rootState.api.backendInteractor.blockUser(id)
+ return store.rootState.api.backendInteractor.blockUser({ id })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
store.commit('addBlockId', id)
@@ -43,12 +43,12 @@ const blockUser = (store, id) => {
}
const unblockUser = (store, id) => {
- return store.rootState.api.backendInteractor.unblockUser(id)
+ return store.rootState.api.backendInteractor.unblockUser({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const muteUser = (store, id) => {
- return store.rootState.api.backendInteractor.muteUser(id)
+ return store.rootState.api.backendInteractor.muteUser({ id })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
store.commit('addMuteId', id)
@@ -56,7 +56,7 @@ const muteUser = (store, id) => {
}
const unmuteUser = (store, id) => {
- return store.rootState.api.backendInteractor.unmuteUser(id)
+ return store.rootState.api.backendInteractor.unmuteUser({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
@@ -324,11 +324,11 @@ const users = {
commit('clearFollowers', userId)
},
subscribeUser ({ rootState, commit }, id) {
- return rootState.api.backendInteractor.subscribeUser(id)
+ return rootState.api.backendInteractor.subscribeUser({ id })
.then((relationship) => commit('updateUserRelationship', [relationship]))
},
unsubscribeUser ({ rootState, commit }, id) {
- return rootState.api.backendInteractor.unsubscribeUser(id)
+ return rootState.api.backendInteractor.unsubscribeUser({ id })
.then((relationship) => commit('updateUserRelationship', [relationship]))
},
toggleActivationStatus ({ rootState, commit }, user) {
@@ -373,8 +373,10 @@ const users = {
},
addNewNotifications (store, { notifications }) {
const users = map(notifications, 'from_profile')
+ const targetUsers = map(notifications, 'target')
const notificationIds = notifications.map(_ => _.id)
store.commit('addNewUsers', users)
+ store.commit('addNewUsers', targetUsers)
const notificationsObject = store.rootState.statuses.notifications.idStore
const relevantNotifications = Object.entries(notificationsObject)
@@ -387,7 +389,7 @@ const users = {
})
},
searchUsers (store, query) {
- return store.rootState.api.backendInteractor.searchUsers(query)
+ return store.rootState.api.backendInteractor.searchUsers({ query })
.then((users) => {
store.commit('addNewUsers', users)
return users
@@ -399,7 +401,7 @@ const users = {
let rootState = store.rootState
try {
- let data = await rootState.api.backendInteractor.register(userInfo)
+ let data = await rootState.api.backendInteractor.register({ ...userInfo })
store.commit('signUpSuccess')
store.commit('setToken', data.access_token)
store.dispatch('loginUser', data.access_token)
@@ -436,10 +438,10 @@ const users = {
store.commit('clearCurrentUser')
store.dispatch('disconnectFromSocket')
store.commit('clearToken')
- store.dispatch('stopFetching', 'friends')
+ store.dispatch('stopFetchingTimeline', 'friends')
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
- store.dispatch('stopFetching', 'notifications')
- store.dispatch('stopFetching', 'followRequest')
+ store.dispatch('stopFetchingNotifications')
+ store.dispatch('stopFetchingFollowRequests')
store.commit('clearNotifications')
store.commit('resetStatuses')
})
@@ -474,11 +476,24 @@ const users = {
store.dispatch('initializeSocket')
}
- // Start getting fresh posts.
- store.dispatch('startFetchingTimeline', { timeline: 'friends' })
+ const startPolling = () => {
+ // Start getting fresh posts.
+ store.dispatch('startFetchingTimeline', { timeline: 'friends' })
- // Start fetching notifications
- store.dispatch('startFetchingNotifications')
+ // Start fetching notifications
+ store.dispatch('startFetchingNotifications')
+ }
+
+ if (store.getters.mergedConfig.useStreamingApi) {
+ store.dispatch('enableMastoSockets').catch((error) => {
+ console.error('Failed initializing MastoAPI Streaming socket', error)
+ startPolling()
+ }).then(() => {
+ setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
+ })
+ } else {
+ startPolling()
+ }
// Get user mutes
store.dispatch('fetchMutes')
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index a69fa53c82..ef0267aaa6 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -72,6 +72,7 @@ const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
const MASTODON_SEARCH_2 = `/api/v2/search`
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
+const MASTODON_STREAMING = '/api/v1/streaming'
const oldfetch = window.fetch
@@ -451,7 +452,7 @@ const deleteRight = ({ right, credentials, ...user }) => {
})
}
-const activateUser = ({ credentials, screen_name: nickname }) => {
+const activateUser = ({ credentials, user: { screen_name: nickname } }) => {
return promisedRequest({
url: ACTIVATE_USER_URL,
method: 'PATCH',
@@ -462,7 +463,7 @@ const activateUser = ({ credentials, screen_name: nickname }) => {
}).then(response => get(response, 'users.0'))
}
-const deactivateUser = ({ credentials, screen_name: nickname }) => {
+const deactivateUser = ({ credentials, user: { screen_name: nickname } }) => {
return promisedRequest({
url: DEACTIVATE_USER_URL,
method: 'PATCH',
@@ -947,6 +948,99 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
})
}
+export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
+ return Object.entries({
+ ...(credentials
+ ? { access_token: credentials }
+ : {}
+ ),
+ stream,
+ ...args
+ }).reduce((acc, [key, val]) => {
+ return acc + `${key}=${val}&`
+ }, MASTODON_STREAMING + '?')
+}
+
+const MASTODON_STREAMING_EVENTS = new Set([
+ 'update',
+ 'notification',
+ 'delete',
+ 'filters_changed'
+])
+
+// A thin wrapper around WebSocket API that allows adding a pre-processor to it
+// Uses EventTarget and a CustomEvent to proxy events
+export const ProcessedWS = ({
+ url,
+ preprocessor = handleMastoWS,
+ id = 'Unknown'
+}) => {
+ const eventTarget = new EventTarget()
+ const socket = new WebSocket(url)
+ if (!socket) throw new Error(`Failed to create socket ${id}`)
+ const proxy = (original, eventName, processor = a => a) => {
+ original.addEventListener(eventName, (eventData) => {
+ eventTarget.dispatchEvent(new CustomEvent(
+ eventName,
+ { detail: processor(eventData) }
+ ))
+ })
+ }
+ socket.addEventListener('open', (wsEvent) => {
+ console.debug(`[WS][${id}] Socket connected`, wsEvent)
+ })
+ socket.addEventListener('error', (wsEvent) => {
+ console.debug(`[WS][${id}] Socket errored`, wsEvent)
+ })
+ socket.addEventListener('close', (wsEvent) => {
+ console.debug(
+ `[WS][${id}] Socket disconnected with code ${wsEvent.code}`,
+ wsEvent
+ )
+ })
+ // Commented code reason: very spammy, uncomment to enable message debug logging
+ /*
+ socket.addEventListener('message', (wsEvent) => {
+ console.debug(
+ `[WS][${id}] Message received`,
+ wsEvent
+ )
+ })
+ /**/
+
+ proxy(socket, 'open')
+ proxy(socket, 'close')
+ proxy(socket, 'message', preprocessor)
+ proxy(socket, 'error')
+
+ // 1000 = Normal Closure
+ eventTarget.close = () => { socket.close(1000, 'Shutting down socket') }
+
+ return eventTarget
+}
+
+export const handleMastoWS = (wsEvent) => {
+ const { data } = wsEvent
+ if (!data) return
+ const parsedEvent = JSON.parse(data)
+ const { event, payload } = parsedEvent
+ if (MASTODON_STREAMING_EVENTS.has(event)) {
+ // MastoBE and PleromaBE both send payload for delete as a PLAIN string
+ if (event === 'delete') {
+ return { event, id: payload }
+ }
+ const data = payload ? JSON.parse(payload) : null
+ if (event === 'update') {
+ return { event, status: parseStatus(data) }
+ } else if (event === 'notification') {
+ return { event, notification: parseNotification(data) }
+ }
+ } else {
+ console.warn('Unknown event', wsEvent)
+ return null
+ }
+}
+
const apiService = {
verifyCredentials,
fetchTimeline,
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 3d4ec6acf6..b7372ed080 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -1,236 +1,39 @@
-import apiService from '../api/api.service.js'
+import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js'
import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js'
import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
-const backendInteractorService = credentials => {
- const fetchStatus = ({ id }) => {
- return apiService.fetchStatus({ id, credentials })
- }
-
- const fetchConversation = ({ id }) => {
- return apiService.fetchConversation({ id, credentials })
- }
-
- const fetchFriends = ({ id, maxId, sinceId, limit }) => {
- return apiService.fetchFriends({ id, maxId, sinceId, limit, credentials })
- }
-
- const exportFriends = ({ id }) => {
- return apiService.exportFriends({ id, credentials })
- }
-
- const fetchFollowers = ({ id, maxId, sinceId, limit }) => {
- return apiService.fetchFollowers({ id, maxId, sinceId, limit, credentials })
- }
-
- const fetchUser = ({ id }) => {
- return apiService.fetchUser({ id, credentials })
- }
-
- const fetchUserRelationship = ({ id }) => {
- return apiService.fetchUserRelationship({ id, credentials })
- }
-
- const followUser = ({ id, reblogs }) => {
- return apiService.followUser({ credentials, id, reblogs })
- }
-
- const unfollowUser = (id) => {
- return apiService.unfollowUser({ credentials, id })
- }
-
- const blockUser = (id) => {
- return apiService.blockUser({ credentials, id })
- }
-
- const unblockUser = (id) => {
- return apiService.unblockUser({ credentials, id })
- }
-
- const approveUser = (id) => {
- return apiService.approveUser({ credentials, id })
- }
-
- const denyUser = (id) => {
- return apiService.denyUser({ credentials, id })
- }
-
- const startFetchingTimeline = ({ timeline, store, userId = false, tag }) => {
+const backendInteractorService = credentials => ({
+ startFetchingTimeline ({ timeline, store, userId = false, tag }) {
return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag })
- }
+ },
- const startFetchingNotifications = ({ store }) => {
+ startFetchingNotifications ({ store }) {
return notificationsFetcher.startFetching({ store, credentials })
- }
+ },
- const startFetchingFollowRequest = ({ store }) => {
+ fetchAndUpdateNotifications ({ store }) {
+ return notificationsFetcher.fetchAndUpdate({ store, credentials })
+ },
+
+ startFetchingFollowRequest ({ store }) {
return followRequestFetcher.startFetching({ store, credentials })
- }
+ },
- // eslint-disable-next-line camelcase
- const tagUser = ({ screen_name }, tag) => {
- return apiService.tagUser({ screen_name, tag, credentials })
- }
+ startUserSocket ({ store }) {
+ const serv = store.rootState.instance.server.replace('http', 'ws')
+ const url = serv + getMastodonSocketURI({ credentials, stream: 'user' })
+ return ProcessedWS({ url, id: 'User' })
+ },
- // eslint-disable-next-line camelcase
- const untagUser = ({ screen_name }, tag) => {
- return apiService.untagUser({ screen_name, tag, credentials })
- }
+ ...Object.entries(apiService).reduce((acc, [key, func]) => {
+ return {
+ ...acc,
+ [key]: (args) => func({ credentials, ...args })
+ }
+ }, {}),
- // eslint-disable-next-line camelcase
- const addRight = ({ screen_name }, right) => {
- return apiService.addRight({ screen_name, right, credentials })
- }
-
- // eslint-disable-next-line camelcase
- const deleteRight = ({ screen_name }, right) => {
- return apiService.deleteRight({ screen_name, right, credentials })
- }
-
- // eslint-disable-next-line camelcase
- const activateUser = ({ screen_name }) => {
- return apiService.activateUser({ screen_name, credentials })
- }
-
- // eslint-disable-next-line camelcase
- const deactivateUser = ({ screen_name }) => {
- return apiService.deactivateUser({ screen_name, credentials })
- }
-
- // eslint-disable-next-line camelcase
- const deleteUser = ({ screen_name }) => {
- return apiService.deleteUser({ screen_name, credentials })
- }
-
- const vote = (pollId, choices) => {
- return apiService.vote({ credentials, pollId, choices })
- }
-
- const fetchPoll = (pollId) => {
- return apiService.fetchPoll({ credentials, pollId })
- }
-
- const updateNotificationSettings = ({ settings }) => {
- return apiService.updateNotificationSettings({ credentials, settings })
- }
-
- const fetchMutes = () => apiService.fetchMutes({ credentials })
- const muteUser = (id) => apiService.muteUser({ credentials, id })
- const unmuteUser = (id) => apiService.unmuteUser({ credentials, id })
- const subscribeUser = (id) => apiService.subscribeUser({ credentials, id })
- const unsubscribeUser = (id) => apiService.unsubscribeUser({ credentials, id })
- const fetchBlocks = () => apiService.fetchBlocks({ credentials })
- const fetchOAuthTokens = () => apiService.fetchOAuthTokens({ credentials })
- const revokeOAuthToken = (id) => apiService.revokeOAuthToken({ id, credentials })
- const fetchPinnedStatuses = (id) => apiService.fetchPinnedStatuses({ credentials, id })
- const pinOwnStatus = (id) => apiService.pinOwnStatus({ credentials, id })
- const unpinOwnStatus = (id) => apiService.unpinOwnStatus({ credentials, id })
- const muteConversation = (id) => apiService.muteConversation({ credentials, id })
- const unmuteConversation = (id) => apiService.unmuteConversation({ credentials, id })
-
- const getCaptcha = () => apiService.getCaptcha()
- const register = (params) => apiService.register({ credentials, params })
- const updateAvatar = ({ avatar }) => apiService.updateAvatar({ credentials, avatar })
- const updateBg = ({ background }) => apiService.updateBg({ credentials, background })
- const updateBanner = ({ banner }) => apiService.updateBanner({ credentials, banner })
- const updateProfile = ({ params }) => apiService.updateProfile({ credentials, params })
-
- const importBlocks = (file) => apiService.importBlocks({ file, credentials })
- const importFollows = (file) => apiService.importFollows({ file, credentials })
-
- const deleteAccount = ({ password }) => apiService.deleteAccount({ credentials, password })
- const changeEmail = ({ email, password }) => apiService.changeEmail({ credentials, email, password })
- const changePassword = ({ password, newPassword, newPasswordConfirmation }) =>
- apiService.changePassword({ credentials, password, newPassword, newPasswordConfirmation })
-
- const fetchSettingsMFA = () => apiService.settingsMFA({ credentials })
- const generateMfaBackupCodes = () => apiService.generateMfaBackupCodes({ credentials })
- const mfaSetupOTP = () => apiService.mfaSetupOTP({ credentials })
- const mfaConfirmOTP = ({ password, token }) => apiService.mfaConfirmOTP({ credentials, password, token })
- const mfaDisableOTP = ({ password }) => apiService.mfaDisableOTP({ credentials, password })
-
- const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({ id })
- const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({ id })
- const reportUser = (params) => apiService.reportUser({ credentials, ...params })
-
- const favorite = (id) => apiService.favorite({ id, credentials })
- const unfavorite = (id) => apiService.unfavorite({ id, credentials })
- const retweet = (id) => apiService.retweet({ id, credentials })
- const unretweet = (id) => apiService.unretweet({ id, credentials })
- const search2 = ({ q, resolve, limit, offset, following }) =>
- apiService.search2({ credentials, q, resolve, limit, offset, following })
- const searchUsers = (query) => apiService.searchUsers({ query, credentials })
-
- const backendInteractorServiceInstance = {
- fetchStatus,
- fetchConversation,
- fetchFriends,
- exportFriends,
- fetchFollowers,
- followUser,
- unfollowUser,
- blockUser,
- unblockUser,
- fetchUser,
- fetchUserRelationship,
- verifyCredentials: apiService.verifyCredentials,
- startFetchingTimeline,
- startFetchingNotifications,
- startFetchingFollowRequest,
- fetchMutes,
- muteUser,
- unmuteUser,
- subscribeUser,
- unsubscribeUser,
- fetchBlocks,
- fetchOAuthTokens,
- revokeOAuthToken,
- fetchPinnedStatuses,
- pinOwnStatus,
- unpinOwnStatus,
- muteConversation,
- unmuteConversation,
- tagUser,
- untagUser,
- addRight,
- deleteRight,
- deleteUser,
- activateUser,
- deactivateUser,
- register,
- getCaptcha,
- updateAvatar,
- updateBg,
- updateBanner,
- updateProfile,
- importBlocks,
- importFollows,
- deleteAccount,
- changeEmail,
- changePassword,
- fetchSettingsMFA,
- generateMfaBackupCodes,
- mfaSetupOTP,
- mfaConfirmOTP,
- mfaDisableOTP,
- approveUser,
- denyUser,
- vote,
- fetchPoll,
- fetchFavoritedByUsers,
- fetchRebloggedByUsers,
- reportUser,
- favorite,
- unfavorite,
- retweet,
- unretweet,
- updateNotificationSettings,
- search2,
- searchUsers
- }
-
- return backendInteractorServiceInstance
-}
+ verifyCredentials: apiService.verifyCredentials
+})
export default backendInteractorService
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index ca79df6f6f..ee007bee12 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -341,10 +341,13 @@ export const parseNotification = (data) => {
if (masto) {
output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen
- output.status = output.type === 'follow'
+ output.status = output.type === 'follow' || output.type === 'move'
? null
: parseStatus(data.status)
output.action = output.status // TODO: Refactor, this is unneeded
+ output.target = output.type !== 'move'
+ ? null
+ : parseUser(data.target)
output.from_profile = parseUser(data.account)
} else {
const parsedNotice = parseStatus(data.notice)
diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js
index 598cb5f726..29b38a0f49 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({
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index 7021adbdd2..b08514da00 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -6,7 +6,8 @@ export const visibleTypes = store => ([
store.state.config.notificationVisibility.likes && 'like',
store.state.config.notificationVisibility.mentions && 'mention',
store.state.config.notificationVisibility.repeats && 'repeat',
- store.state.config.notificationVisibility.follows && 'follow'
+ store.state.config.notificationVisibility.follows && 'follow',
+ store.state.config.notificationVisibility.moves && 'move'
].filter(_ => _))
const sortById = (a, b) => {
diff --git a/src/services/push/push.js b/src/services/push/push.js
index 1b189a2933..5836fc2626 100644
--- a/src/services/push/push.js
+++ b/src/services/push/push.js
@@ -65,7 +65,8 @@ function sendSubscriptionToBackEnd (subscription, token, notificationVisibility)
follow: notificationVisibility.follows,
favourite: notificationVisibility.likes,
mention: notificationVisibility.mentions,
- reblog: notificationVisibility.repeats
+ reblog: notificationVisibility.repeats,
+ move: notificationVisibility.moves
}
}
})
diff --git a/static/fontello.json b/static/fontello.json
index c1ed339383..829241b55d 100755
--- a/static/fontello.json
+++ b/static/fontello.json
@@ -333,6 +333,12 @@
"css": "login",
"code": 59424,
"src": "fontawesome"
+ },
+ {
+ "uid": "f3ebd6751c15a280af5cc5f4a764187d",
+ "css": "arrow-curved",
+ "code": 59426,
+ "src": "iconic"
}
]
}
\ No newline at end of file