diff options
Diffstat (limited to 'src/modules')
| -rw-r--r-- | src/modules/api.js | 141 | ||||
| -rw-r--r-- | src/modules/auth_flow.js | 8 | ||||
| -rw-r--r-- | src/modules/config.js | 30 | ||||
| -rw-r--r-- | src/modules/instance.js | 98 | ||||
| -rw-r--r-- | src/modules/interface.js | 32 | ||||
| -rw-r--r-- | src/modules/oauth_tokens.js | 2 | ||||
| -rw-r--r-- | src/modules/polls.js | 4 | ||||
| -rw-r--r-- | src/modules/statuses.js | 197 | ||||
| -rw-r--r-- | src/modules/users.js | 153 |
9 files changed, 535 insertions, 130 deletions
diff --git a/src/modules/api.js b/src/modules/api.js index 1293e3c8..748570e5 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,135 @@ 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 }) + }, + + // Follow requests + startFetchingFollowRequests (store) { + if (store.state.fetchers['followRequests']) return + const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store }) - const fetcher = store.state.backendInteractor.startFetchingFollowRequest({ store }) - store.commit('addFetcher', { fetcherName: 'followRequest', fetcher }) + 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 +177,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/auth_flow.js b/src/modules/auth_flow.js index d0a90feb..956d40e8 100644 --- a/src/modules/auth_flow.js +++ b/src/modules/auth_flow.js @@ -7,7 +7,6 @@ const RECOVERY_STRATEGY = 'recovery' // initial state const state = { - app: null, settings: {}, strategy: PASSWORD_STRATEGY, initStrategy: PASSWORD_STRATEGY // default strategy from config @@ -16,14 +15,10 @@ const state = { const resetState = (state) => { state.strategy = state.initStrategy state.settings = {} - state.app = null } // getters const getters = { - app: (state, getters) => { - return state.app - }, settings: (state, getters) => { return state.settings }, @@ -55,9 +50,8 @@ const mutations = { requireToken (state) { state.strategy = TOKEN_STRATEGY }, - requireMFA (state, { app, settings }) { + requireMFA (state, { settings }) { state.settings = settings - state.app = app state.strategy = TOTP_STRATEGY // default strategy of MFA }, requireRecovery (state) { diff --git a/src/modules/config.js b/src/modules/config.js index 329b4091..47b24d77 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -1,10 +1,24 @@ import { set, delete as del } from 'vue' import { setPreset, applyTheme } from '../services/style_setter/style_setter.js' +import messages from '../i18n/messages' const browserLocale = (window.navigator.language || 'en').split('-')[0] +/* TODO this is a bit messy. + * We need to declare settings with their types and also deal with + * instance-default settings in some way, hopefully try to avoid copy-pasta + * in general. + */ +export const multiChoiceProperties = [ + 'postContentType', + 'subjectLineBehavior' +] + export const defaultState = { colors: {}, + theme: undefined, + customTheme: undefined, + customThemeSource: undefined, hideISP: false, // bad name: actually hides posts of muted USERS hideMutedPosts: undefined, // instance default @@ -20,6 +34,7 @@ export const defaultState = { autoLoad: true, streaming: false, hoverPreview: true, + emojiReactionsOnTimeline: true, autohideFloatingPostButton: false, pauseOnUnfocused: true, stopGifs: false, @@ -28,13 +43,17 @@ export const defaultState = { follows: true, mentions: true, likes: true, - repeats: true + repeats: true, + moves: true, + emojiReactions: false, + followRequest: true }, webPushNotifications: false, muteWords: [], highlight: {}, interfaceLanguage: browserLocale, hideScopeNotice: false, + useStreamingApi: false, scopeCopy: undefined, // instance default subjectLineBehavior: undefined, // instance default alwaysShowSubjectInput: undefined, // instance default @@ -92,10 +111,15 @@ const config = { commit('setOption', { name, value }) switch (name) { case 'theme': - setPreset(value, commit) + setPreset(value) break case 'customTheme': - applyTheme(value, commit) + case 'customThemeSource': + applyTheme(value) + break + case 'interfaceLanguage': + messages.setLanguage(this.getters.i18n, value) + break } } } diff --git a/src/modules/instance.js b/src/modules/instance.js index 96f14ed5..ec5f4e54 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -1,52 +1,60 @@ import { set } from 'vue' -import { setPreset } from '../services/style_setter/style_setter.js' +import { getPreset, applyTheme } from '../services/style_setter/style_setter.js' +import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' +import apiService from '../services/api/api.service.js' import { instanceDefaultProperties } from './config.js' const defaultState = { - // Stuff from static/config.json and apiConfig + // Stuff from apiConfig name: 'Pleroma FE', registrationOpen: true, - safeDM: true, - textlimit: 5000, server: 'http://localhost:4040/', - theme: 'pleroma-dark', - background: '/static/aurora_borealis.jpg', - logo: '/static/logo.png', - logoMask: true, - logoMargin: '.2em', - redirectRootNoLogin: '/main/all', - redirectRootLogin: '/main/friends', - showInstanceSpecificPanel: false, + textlimit: 5000, + themeData: undefined, + vapidPublicKey: undefined, + + // Stuff from static/config.json alwaysShowSubjectInput: true, - hideMutedPosts: false, + background: '/static/aurora_borealis.jpg', collapseMessageWithSubject: false, + disableChat: false, + greentext: false, + hideFilteredStatuses: false, + hideMutedPosts: false, hidePostStats: false, + hideSitename: false, hideUserStats: false, - hideFilteredStatuses: false, - disableChat: false, - scopeCopy: true, - subjectLineBehavior: 'email', - postContentType: 'text/plain', + loginMethod: 'password', + logo: '/static/logo.png', + logoMargin: '.2em', + logoMask: true, + minimalScopesMode: false, nsfwCensorImage: undefined, - vapidPublicKey: undefined, - noAttachmentLinks: false, + postContentType: 'text/plain', + redirectRootLogin: '/main/friends', + redirectRootNoLogin: '/main/all', + scopeCopy: true, showFeaturesPanel: true, - minimalScopesMode: false, - greentext: false, + showInstanceSpecificPanel: false, + sidebarRight: false, + subjectLineBehavior: 'email', + theme: 'pleroma-dark', // Nasty stuff - pleromaBackend: true, - emoji: [], - emojiFetched: false, customEmoji: [], customEmojiFetched: false, - restrictedNicknames: [], + emoji: [], + emojiFetched: false, + pleromaBackend: true, postFormats: [], + restrictedNicknames: [], + safeDM: true, + knownDomains: [], // Feature-set, apparently, not everything here is reported... - mediaProxyAvailable: false, chatAvailable: false, gopherAvailable: false, + mediaProxyAvailable: false, suggestionsEnabled: false, suggestionsWeb: '', @@ -74,6 +82,9 @@ const instance = { if (typeof value !== 'undefined') { set(state, name, value) } + }, + setKnownDomains (state, domains) { + state.knownDomains = domains } }, getters: { @@ -95,6 +106,9 @@ const instance = { dispatch('initializeSocket') } break + case 'theme': + dispatch('setTheme', value) + break } }, async getStaticEmoji ({ commit }) { @@ -146,9 +160,23 @@ const instance = { } }, - setTheme ({ commit }, themeName) { + setTheme ({ commit, rootState }, themeName) { commit('setInstanceOption', { name: 'theme', value: themeName }) - return setPreset(themeName, commit) + getPreset(themeName) + .then(themeData => { + commit('setInstanceOption', { name: 'themeData', value: themeData }) + // No need to apply theme if there's user theme already + const { customTheme } = rootState.config + if (customTheme) return + + // New theme presets don't have 'theme' property, they use 'source' + const themeSource = themeData.source + if (!themeData.theme || (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION)) { + applyTheme(themeSource) + } else { + applyTheme(themeData.theme) + } + }) }, fetchEmoji ({ dispatch, state }) { if (!state.customEmojiFetched) { @@ -159,6 +187,18 @@ const instance = { state.emojiFetched = true dispatch('getStaticEmoji') } + }, + + async getKnownDomains ({ commit, rootState }) { + try { + const result = await apiService.fetchKnownDomains({ + credentials: rootState.users.currentUser.credentials + }) + commit('setKnownDomains', result) + } catch (e) { + console.warn("Can't load known domains") + console.warn(e) + } } } } diff --git a/src/modules/interface.js b/src/modules/interface.js index 5b2762e5..eeebd65e 100644 --- a/src/modules/interface.js +++ b/src/modules/interface.js @@ -1,6 +1,8 @@ import { set, delete as del } from 'vue' const defaultState = { + settingsModalState: 'hidden', + settingsModalLoaded: false, settings: { currentSaveStateNotice: null, noticeClearTimeout: null, @@ -35,6 +37,27 @@ const interfaceMod = { }, setMobileLayout (state, value) { state.mobileLayout = value + }, + closeSettingsModal (state) { + state.settingsModalState = 'hidden' + }, + togglePeekSettingsModal (state) { + switch (state.settingsModalState) { + case 'minimized': + state.settingsModalState = 'visible' + return + case 'visible': + state.settingsModalState = 'minimized' + return + default: + throw new Error('Illegal minimization state of settings modal') + } + }, + openSettingsModal (state) { + state.settingsModalState = 'visible' + if (!state.settingsModalLoaded) { + state.settingsModalLoaded = true + } } }, actions: { @@ -49,6 +72,15 @@ const interfaceMod = { }, setMobileLayout ({ commit }, value) { commit('setMobileLayout', value) + }, + closeSettingsModal ({ commit }) { + commit('closeSettingsModal') + }, + openSettingsModal ({ commit }) { + commit('openSettingsModal') + }, + togglePeekSettingsModal ({ commit }) { + commit('togglePeekSettingsModal') } } } 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..9a2e0df1 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -1,7 +1,21 @@ -import { remove, slice, each, findIndex, find, maxBy, minBy, merge, first, last, isArray, omitBy } from 'lodash' +import { + remove, + slice, + each, + findIndex, + find, + maxBy, + minBy, + merge, + first, + last, + isArray, + omitBy +} from 'lodash' import { set } from 'vue' +import { isStatusNotification } from '../services/notification_utils/notification_utils.js' import apiService from '../services/api/api.service.js' -// import parse from '../services/status_parser/status_parser.js' +import { muteWordHits } from '../services/status_parser/status_parser.js' const emptyTl = (userId = 0) => ({ statuses: [], @@ -38,6 +52,7 @@ export const defaultState = () => ({ notifications: emptyNotifications(), favorites: new Set(), error: false, + errorData: null, timelines: { mentions: emptyTl(), public: emptyTl(), @@ -66,7 +81,9 @@ 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', + rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reactions' ].filter(_ => _) } @@ -305,11 +322,15 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => { each(notifications, (notification) => { - if (notification.type !== 'follow') { + if (isStatusNotification(notification.type)) { notification.action = addStatusToGlobalStorage(state, notification.action).item notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item } + if (notification.type === 'pleroma:emoji_reaction') { + dispatch('fetchEmojiReactionsBy', notification.status.id) + } + // Only add a new notification if we don't have one for the same action if (!state.notifications.idStore.hasOwnProperty(notification.id)) { state.notifications.maxId = notification.id > state.notifications.maxId @@ -338,11 +359,19 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot case 'follow': i18nString = 'followed_you' break + case 'move': + i18nString = 'migrated_to' + break + case 'follow_request': + i18nString = 'follow_request' + break } - if (i18nString) { + if (notification.type === 'pleroma:emoji_reaction') { + notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji]) + } else if (i18nString) { notifObj.body = rootGetters.i18n.t('notifications.' + i18nString) - } else { + } else if (isStatusNotification(notification.type)) { notifObj.body = notification.status.text } @@ -352,11 +381,22 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot notifObj.image = status.attachments[0].url } - if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) { - let notification = new window.Notification(title, notifObj) + const reasonsToMuteNotif = ( + notification.seen || + state.notifications.desktopNotificationSilence || + !visibleNotificationTypes.includes(notification.type) || + ( + notification.type === 'mention' && status && ( + status.muted || + muteWordHits(status, rootGetters.mergedConfig.muteWords).length === 0 + ) + ) + ) + if (!reasonsToMuteNotif) { + let desktopNotification = new window.Notification(title, notifObj) // Chrome is known for not closing notifications automatically // according to MDN, anyway. - setTimeout(notification.close.bind(notification), 5000) + setTimeout(desktopNotification.close.bind(desktopNotification), 5000) } } } else if (notification.seen) { @@ -479,6 +519,9 @@ export const mutations = { setError (state, { value }) { state.error = value }, + setErrorData (state, { value }) { + state.errorData = value + }, setNotificationsLoading (state, { value }) { state.notifications.loading = value }, @@ -493,6 +536,17 @@ export const mutations = { notification.seen = true }) }, + markSingleNotificationAsSeen (state, { id }) { + const notification = find(state.notifications.data, n => n.id === id) + if (notification) notification.seen = true + }, + dismissNotification (state, { id }) { + state.notifications.data = state.notifications.data.filter(n => n.id !== id) + }, + updateNotification (state, { id, updater }) { + const notification = find(state.notifications.data, n => n.id === id) + notification && updater(notification) + }, queueFlush (state, { timeline, id }) { state.timelines[timeline].flushMarker = id }, @@ -510,6 +564,53 @@ export const mutations = { newStatus.fave_num = newStatus.favoritedBy.length newStatus.favorited = !!newStatus.favoritedBy.find(({ id }) => currentUser.id === id) }, + addEmojiReactionsBy (state, { id, emojiReactions, currentUser }) { + const status = state.allStatusesObject[id] + set(status, 'emoji_reactions', emojiReactions) + }, + addOwnReaction (state, { id, emoji, currentUser }) { + const status = state.allStatusesObject[id] + const reactionIndex = findIndex(status.emoji_reactions, { name: emoji }) + const reaction = status.emoji_reactions[reactionIndex] || { name: emoji, count: 0, accounts: [] } + + const newReaction = { + ...reaction, + count: reaction.count + 1, + me: true, + accounts: [ + ...reaction.accounts, + currentUser + ] + } + + // Update count of existing reaction if it exists, otherwise append at the end + if (reactionIndex >= 0) { + set(status.emoji_reactions, reactionIndex, newReaction) + } else { + set(status, 'emoji_reactions', [...status.emoji_reactions, newReaction]) + } + }, + removeOwnReaction (state, { id, emoji, currentUser }) { + const status = state.allStatusesObject[id] + const reactionIndex = findIndex(status.emoji_reactions, { name: emoji }) + if (reactionIndex < 0) return + + const reaction = status.emoji_reactions[reactionIndex] + const accounts = reaction.accounts || [] + + const newReaction = { + ...reaction, + count: reaction.count - 1, + me: false, + accounts: accounts.filter(acc => acc.id !== currentUser.id) + } + + if (newReaction.count > 0) { + set(status.emoji_reactions, reactionIndex, newReaction) + } else { + set(status, 'emoji_reactions', status.emoji_reactions.filter(r => r.name !== emoji)) + } + }, updateStatusWithPoll (state, { id, poll }) { const status = state.allStatusesObject[id] status.poll = poll @@ -528,6 +629,9 @@ const statuses = { setError ({ rootState, commit }, { value }) { commit('setError', { value }) }, + setErrorData ({ rootState, commit }, { value }) { + commit('setErrorData', { value }) + }, setNotificationsLoading ({ rootState, commit }, { value }) { commit('setNotificationsLoading', { value }) }, @@ -538,7 +642,7 @@ const statuses = { commit('setNotificationsSilence', { value }) }, fetchStatus ({ rootState, dispatch }, id) { - rootState.api.backendInteractor.fetchStatus({ id }) + return rootState.api.backendInteractor.fetchStatus({ id }) .then((status) => dispatch('addNewStatuses', { statuses: [status] })) }, deleteStatus ({ rootState, commit }, status) { @@ -551,45 +655,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 }) { @@ -602,21 +706,68 @@ const statuses = { credentials: rootState.users.currentUser.credentials }) }, + markSingleNotificationAsSeen ({ rootState, commit }, { id }) { + commit('markSingleNotificationAsSeen', { id }) + apiService.markNotificationsAsSeen({ + single: true, + id, + credentials: rootState.users.currentUser.credentials + }) + }, + dismissNotificationLocal ({ rootState, commit }, { id }) { + commit('dismissNotification', { id }) + }, + dismissNotification ({ rootState, commit }, { id }) { + commit('dismissNotification', { id }) + rootState.api.backendInteractor.dismissNotification({ id }) + }, + updateNotification ({ rootState, commit }, { id, updater }) { + commit('updateNotification', { id, updater }) + }, 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 }) }) }, + reactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) { + const currentUser = rootState.users.currentUser + if (!currentUser) return + + commit('addOwnReaction', { id, emoji, currentUser }) + rootState.api.backendInteractor.reactWithEmoji({ id, emoji }).then( + ok => { + dispatch('fetchEmojiReactionsBy', id) + } + ) + }, + unreactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) { + const currentUser = rootState.users.currentUser + if (!currentUser) return + + commit('removeOwnReaction', { id, emoji, currentUser }) + rootState.api.backendInteractor.unreactWithEmoji({ id, emoji }).then( + ok => { + dispatch('fetchEmojiReactionsBy', id) + } + ) + }, + fetchEmojiReactionsBy ({ rootState, commit }, id) { + rootState.api.backendInteractor.fetchEmojiReactions({ id }).then( + emojiReactions => { + commit('addEmojiReactionsBy', { id, emojiReactions, 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..f9329f2a 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,17 @@ 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) + const predictedRelationship = store.state.relationships[id] || { id } + predictedRelationship.muting = true + store.commit('updateUserRelationship', [predictedRelationship]) + store.commit('addMuteId', id) + + return store.rootState.api.backendInteractor.muteUser({ id }) .then((relationship) => { store.commit('updateUserRelationship', [relationship]) store.commit('addMuteId', id) @@ -56,7 +61,11 @@ const muteUser = (store, id) => { } const unmuteUser = (store, id) => { - return store.rootState.api.backendInteractor.unmuteUser(id) + const predictedRelationship = store.state.relationships[id] || { id } + predictedRelationship.muting = false + store.commit('updateUserRelationship', [predictedRelationship]) + + return store.rootState.api.backendInteractor.unmuteUser({ id }) .then((relationship) => store.commit('updateUserRelationship', [relationship])) } @@ -72,11 +81,17 @@ const showReblogs = (store, userId) => { .then((relationship) => store.commit('updateUserRelationship', [relationship])) } +const muteDomain = (store, domain) => { + return store.rootState.api.backendInteractor.muteDomain({ domain }) + .then(() => store.commit('addDomainMute', domain)) +} + +const unmuteDomain = (store, domain) => { + return store.rootState.api.backendInteractor.unmuteDomain({ domain }) + .then(() => store.commit('removeDomainMute', domain)) +} + export const mutations = { - setMuted (state, { user: { id }, muted }) { - const user = state.usersObject[id] - set(user, 'muted', muted) - }, tagUser (state, { user: { id }, tag }) { const user = state.usersObject[id] const tags = user.tags || [] @@ -95,9 +110,9 @@ export const mutations = { newRights[right] = value set(user, 'rights', newRights) }, - updateActivationStatus (state, { user: { id }, status }) { + updateActivationStatus (state, { user: { id }, deactivated }) { const user = state.usersObject[id] - set(user, 'deactivated', !status) + set(user, 'deactivated', deactivated) }, setCurrentUser (state, user) { state.lastLoginName = user.screen_name @@ -136,26 +151,18 @@ export const mutations = { } }, addNewUsers (state, users) { - each(users, (user) => mergeOrAdd(state.users, state.usersObject, user)) + each(users, (user) => { + if (user.relationship) { + set(state.relationships, user.relationship.id, user.relationship) + } + mergeOrAdd(state.users, state.usersObject, user) + }) }, updateUserRelationship (state, relationships) { relationships.forEach((relationship) => { - const user = state.usersObject[relationship.id] - if (user) { - user.follows_you = relationship.followed_by - user.following = relationship.following - user.muted = relationship.muting - user.statusnet_blocking = relationship.blocking - user.subscribed = relationship.subscribing - user.showing_reblogs = relationship.showing_reblogs - } + set(state.relationships, relationship.id, relationship) }) }, - updateBlocks (state, blockedUsers) { - // Reset statusnet_blocking of all fetched users - each(state.users, (user) => { user.statusnet_blocking = false }) - each(blockedUsers, (user) => mergeOrAdd(state.users, state.usersObject, user)) - }, saveBlockIds (state, blockIds) { state.currentUser.blockIds = blockIds }, @@ -164,11 +171,6 @@ export const mutations = { state.currentUser.blockIds.push(blockId) } }, - updateMutes (state, mutedUsers) { - // Reset muted of all fetched users - each(state.users, (user) => { user.muted = false }) - each(mutedUsers, (user) => mergeOrAdd(state.users, state.usersObject, user)) - }, saveMuteIds (state, muteIds) { state.currentUser.muteIds = muteIds }, @@ -177,6 +179,20 @@ export const mutations = { state.currentUser.muteIds.push(muteId) } }, + saveDomainMutes (state, domainMutes) { + state.currentUser.domainMutes = domainMutes + }, + addDomainMute (state, domain) { + if (state.currentUser.domainMutes.indexOf(domain) === -1) { + state.currentUser.domainMutes.push(domain) + } + }, + removeDomainMute (state, domain) { + const index = state.currentUser.domainMutes.indexOf(domain) + if (index !== -1) { + state.currentUser.domainMutes.splice(index, 1) + } + }, setPinnedToUser (state, status) { const user = state.usersObject[status.user.id] const index = user.pinnedStatusIds.indexOf(status.id) @@ -220,6 +236,10 @@ export const getters = { return state.usersObject[query.toLowerCase()] } return result + }, + relationship: state => id => { + const rel = id && state.relationships[id] + return rel || { id, loading: true } } } @@ -230,7 +250,8 @@ export const defaultState = { users: [], usersObject: {}, signUpPending: false, - signUpErrors: [] + signUpErrors: [], + relationships: {} } const users = { @@ -255,7 +276,7 @@ const users = { return store.rootState.api.backendInteractor.fetchBlocks() .then((blocks) => { store.commit('saveBlockIds', map(blocks, 'id')) - store.commit('updateBlocks', blocks) + store.commit('addNewUsers', blocks) return blocks }) }, @@ -274,8 +295,8 @@ const users = { fetchMutes (store) { return store.rootState.api.backendInteractor.fetchMutes() .then((mutes) => { - store.commit('updateMutes', mutes) store.commit('saveMuteIds', map(mutes, 'id')) + store.commit('addNewUsers', mutes) return mutes }) }, @@ -297,6 +318,25 @@ const users = { unmuteUsers (store, ids = []) { return Promise.all(ids.map(id => unmuteUser(store, id))) }, + fetchDomainMutes (store) { + return store.rootState.api.backendInteractor.fetchDomainMutes() + .then((domainMutes) => { + store.commit('saveDomainMutes', domainMutes) + return domainMutes + }) + }, + muteDomain (store, domain) { + return muteDomain(store, domain) + }, + unmuteDomain (store, domain) { + return unmuteDomain(store, domain) + }, + muteDomains (store, domains = []) { + return Promise.all(domains.map(domain => muteDomain(store, domain))) + }, + unmuteDomains (store, domain = []) { + return Promise.all(domain.map(domain => unmuteDomain(store, domain))) + }, fetchFriends ({ rootState, commit }, id) { const user = rootState.users.usersObject[id] const maxId = last(user.friendIds) @@ -324,13 +364,18 @@ 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 }) { + const api = user.deactivated ? rootState.api.backendInteractor.activateUser : rootState.api.backendInteractor.deactivateUser + api({ user }) + .then(({ deactivated }) => commit('updateActivationStatus', { user, deactivated })) + }, registerPushNotifications (store) { const token = store.state.currentUser.credentials const vapidPublicKey = store.rootState.instance.vapidPublicKey @@ -368,8 +413,10 @@ const users = { }, addNewNotifications (store, { notifications }) { const users = map(notifications, 'from_profile') + const targetUsers = map(notifications, 'target').filter(_ => _) 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) @@ -381,8 +428,8 @@ const users = { store.commit('setUserForNotification', notification) }) }, - searchUsers (store, query) { - return store.rootState.api.backendInteractor.searchUsers(query) + searchUsers (store, { query }) { + return store.rootState.api.backendInteractor.searchUsers({ query }) .then((users) => { store.commit('addNewUsers', users) return users @@ -394,7 +441,9 @@ const users = { let rootState = store.rootState try { - let data = await rootState.api.backendInteractor.register(userInfo) + let data = await rootState.api.backendInteractor.register( + { params: { ...userInfo } } + ) store.commit('signUpSuccess') store.commit('setToken', data.access_token) store.dispatch('loginUser', data.access_token) @@ -431,10 +480,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') }) @@ -451,6 +500,7 @@ const users = { user.credentials = accessToken user.blockIds = [] user.muteIds = [] + user.domainMutes = [] commit('setCurrentUser', user) commit('addNewUsers', [user]) @@ -469,11 +519,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') |
