aboutsummaryrefslogtreecommitdiff
path: root/src/modules
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules')
-rw-r--r--src/modules/api.js141
-rw-r--r--src/modules/auth_flow.js8
-rw-r--r--src/modules/config.js30
-rw-r--r--src/modules/instance.js98
-rw-r--r--src/modules/interface.js32
-rw-r--r--src/modules/oauth_tokens.js2
-rw-r--r--src/modules/polls.js4
-rw-r--r--src/modules/statuses.js197
-rw-r--r--src/modules/users.js153
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')