aboutsummaryrefslogtreecommitdiff
path: root/src/modules
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules')
-rw-r--r--src/modules/api.js29
-rw-r--r--src/modules/auth_flow.js90
-rw-r--r--src/modules/chat.js6
-rw-r--r--src/modules/config.js6
-rw-r--r--src/modules/errors.js1
-rw-r--r--src/modules/instance.js14
-rw-r--r--src/modules/interface.js1
-rw-r--r--src/modules/oauth.js45
-rw-r--r--src/modules/oauth_tokens.js4
-rw-r--r--src/modules/polls.js70
-rw-r--r--src/modules/reports.js30
-rw-r--r--src/modules/statuses.js254
-rw-r--r--src/modules/users.js301
13 files changed, 660 insertions, 191 deletions
diff --git a/src/modules/api.js b/src/modules/api.js
index 31cb55c6..d51b31f3 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -13,11 +13,11 @@ const api = {
setBackendInteractor (state, backendInteractor) {
state.backendInteractor = backendInteractor
},
- addFetcher (state, {timeline, fetcher}) {
- state.fetchers[timeline] = fetcher
+ addFetcher (state, { fetcherName, fetcher }) {
+ state.fetchers[fetcherName] = fetcher
},
- removeFetcher (state, {timeline}) {
- delete state.fetchers[timeline]
+ removeFetcher (state, { fetcherName }) {
+ delete state.fetchers[fetcherName]
},
setWsToken (state, token) {
state.wsToken = token
@@ -33,17 +33,24 @@ const api = {
}
},
actions: {
- startFetching (store, {timeline = 'friends', tag = false, userId = false}) {
+ startFetchingTimeline (store, { timeline = 'friends', tag = false, userId = false }) {
// Don't start fetching if we already are.
if (store.state.fetchers[timeline]) return
- const fetcher = store.state.backendInteractor.startFetching({ timeline, store, userId, tag })
- store.commit('addFetcher', { timeline, fetcher })
+ const fetcher = store.state.backendInteractor.startFetchingTimeline({ timeline, store, userId, tag })
+ store.commit('addFetcher', { fetcherName: timeline, fetcher })
},
- stopFetching (store, timeline) {
- const fetcher = store.state.fetchers[timeline]
+ startFetchingNotifications (store) {
+ // Don't start fetching if we already are.
+ if (store.state.fetchers['notifications']) return
+
+ const fetcher = store.state.backendInteractor.startFetchingNotifications({ store })
+ store.commit('addFetcher', { fetcherName: 'notifications', fetcher })
+ },
+ stopFetching (store, fetcherName) {
+ const fetcher = store.state.fetchers[fetcherName]
window.clearInterval(fetcher)
- store.commit('removeFetcher', {timeline})
+ store.commit('removeFetcher', { fetcherName })
},
setWsToken (store, token) {
store.commit('setWsToken', token)
@@ -52,7 +59,7 @@ const api = {
// Set up websocket connection
if (!store.state.chatDisabled) {
const token = store.state.wsToken
- const socket = new Socket('/socket', {params: {token}})
+ const socket = new Socket('/socket', { params: { token } })
socket.connect()
store.dispatch('initializeChat', socket)
}
diff --git a/src/modules/auth_flow.js b/src/modules/auth_flow.js
new file mode 100644
index 00000000..d0a90feb
--- /dev/null
+++ b/src/modules/auth_flow.js
@@ -0,0 +1,90 @@
+const PASSWORD_STRATEGY = 'password'
+const TOKEN_STRATEGY = 'token'
+
+// MFA strategies
+const TOTP_STRATEGY = 'totp'
+const RECOVERY_STRATEGY = 'recovery'
+
+// initial state
+const state = {
+ app: null,
+ settings: {},
+ strategy: PASSWORD_STRATEGY,
+ initStrategy: PASSWORD_STRATEGY // default strategy from config
+}
+
+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
+ },
+ requiredPassword: (state, getters, rootState) => {
+ return state.strategy === PASSWORD_STRATEGY
+ },
+ requiredToken: (state, getters, rootState) => {
+ return state.strategy === TOKEN_STRATEGY
+ },
+ requiredTOTP: (state, getters, rootState) => {
+ return state.strategy === TOTP_STRATEGY
+ },
+ requiredRecovery: (state, getters, rootState) => {
+ return state.strategy === RECOVERY_STRATEGY
+ }
+}
+
+// mutations
+const mutations = {
+ setInitialStrategy (state, strategy) {
+ if (strategy) {
+ state.initStrategy = strategy
+ state.strategy = strategy
+ }
+ },
+ requirePassword (state) {
+ state.strategy = PASSWORD_STRATEGY
+ },
+ requireToken (state) {
+ state.strategy = TOKEN_STRATEGY
+ },
+ requireMFA (state, { app, settings }) {
+ state.settings = settings
+ state.app = app
+ state.strategy = TOTP_STRATEGY // default strategy of MFA
+ },
+ requireRecovery (state) {
+ state.strategy = RECOVERY_STRATEGY
+ },
+ requireTOTP (state) {
+ state.strategy = TOTP_STRATEGY
+ },
+ abortMFA (state) {
+ resetState(state)
+ }
+}
+
+// actions
+const actions = {
+ // eslint-disable-next-line camelcase
+ async login ({ state, dispatch, commit }, { access_token }) {
+ commit('setToken', access_token, { root: true })
+ await dispatch('loginUser', access_token, { root: true })
+ resetState(state)
+ }
+}
+
+export default {
+ namespaced: true,
+ state,
+ getters,
+ mutations,
+ actions
+}
diff --git a/src/modules/chat.js b/src/modules/chat.js
index 2804e577..e1b03bca 100644
--- a/src/modules/chat.js
+++ b/src/modules/chat.js
@@ -1,7 +1,7 @@
const chat = {
state: {
messages: [],
- channel: {state: ''},
+ channel: { state: '' },
socket: null
},
mutations: {
@@ -21,7 +21,7 @@ const chat = {
},
actions: {
disconnectFromChat (store) {
- store.state.socket.disconnect()
+ store.state.socket && store.state.socket.disconnect()
},
initializeChat (store, socket) {
const channel = socket.channel('chat:public')
@@ -29,7 +29,7 @@ const chat = {
channel.on('new_msg', (msg) => {
store.commit('addMessage', msg)
})
- channel.on('messages', ({messages}) => {
+ channel.on('messages', ({ messages }) => {
store.commit('setMessages', messages)
})
channel.join()
diff --git a/src/modules/config.js b/src/modules/config.js
index 1666a2c5..2bfad8f6 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -17,6 +17,7 @@ const defaultState = {
autoLoad: true,
streaming: false,
hoverPreview: true,
+ autohideFloatingPostButton: false,
pauseOnUnfocused: true,
stopGifs: false,
replyVisibility: 'all',
@@ -30,6 +31,7 @@ const defaultState = {
muteWords: [],
highlight: {},
interfaceLanguage: browserLocale,
+ hideScopeNotice: false,
scopeCopy: undefined, // instance default
subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined, // instance default
@@ -54,10 +56,10 @@ const config = {
},
actions: {
setHighlight ({ commit, dispatch }, { user, color, type }) {
- commit('setHighlight', {user, color, type})
+ commit('setHighlight', { user, color, type })
},
setOption ({ commit, dispatch }, { name, value }) {
- commit('setOption', {name, value})
+ commit('setOption', { name, value })
switch (name) {
case 'theme':
setPreset(value, commit)
diff --git a/src/modules/errors.js b/src/modules/errors.js
index c809e1b5..ca89dc0f 100644
--- a/src/modules/errors.js
+++ b/src/modules/errors.js
@@ -9,4 +9,3 @@ export function humanizeErrors (errors) {
return [...errs, message]
}, [])
}
-
diff --git a/src/modules/instance.js b/src/modules/instance.js
index d4185f6a..93b56577 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -16,7 +16,6 @@ const defaultState = {
redirectRootNoLogin: '/main/all',
redirectRootLogin: '/main/friends',
showInstanceSpecificPanel: false,
- formattingOptionsEnabled: false,
alwaysShowSubjectInput: true,
hideMutedPosts: false,
collapseMessageWithSubject: false,
@@ -27,7 +26,6 @@ const defaultState = {
scopeCopy: true,
subjectLineBehavior: 'email',
postContentType: 'text/plain',
- loginMethod: 'password',
nsfwCensorImage: undefined,
vapidPublicKey: undefined,
noAttachmentLinks: false,
@@ -54,7 +52,15 @@ const defaultState = {
// Version Information
backendVersion: '',
- frontendVersion: ''
+ frontendVersion: '',
+
+ pollsAvailable: false,
+ pollLimits: {
+ max_options: 4,
+ max_option_chars: 255,
+ min_expiration: 60,
+ max_expiration: 60 * 60 * 24
+ }
}
const instance = {
@@ -68,7 +74,7 @@ const instance = {
},
actions: {
setInstanceOption ({ commit, dispatch }, { name, value }) {
- commit('setInstanceOption', {name, value})
+ commit('setInstanceOption', { name, value })
switch (name) {
case 'name':
dispatch('setPageTitle')
diff --git a/src/modules/interface.js b/src/modules/interface.js
index 71554787..5b2762e5 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -48,7 +48,6 @@ const interfaceMod = {
commit('setNotificationPermission', permission)
},
setMobileLayout ({ commit }, value) {
- console.log('setMobileLayout called')
commit('setMobileLayout', value)
}
}
diff --git a/src/modules/oauth.js b/src/modules/oauth.js
index 144ff830..a2a83450 100644
--- a/src/modules/oauth.js
+++ b/src/modules/oauth.js
@@ -1,16 +1,47 @@
+import { delete as del } from 'vue'
+
const oauth = {
state: {
- client_id: false,
- client_secret: false,
- token: false
+ clientId: false,
+ clientSecret: false,
+ /* App token is authentication for app without any user, used mostly for
+ * MastoAPI's registration of new users, stored so that we can fall back to
+ * it on logout
+ */
+ appToken: false,
+ /* User token is authentication for app with user, this is for every calls
+ * that need authorized user to be successful (i.e. posting, liking etc)
+ */
+ userToken: false
},
mutations: {
- setClientData (state, data) {
- state.client_id = data.client_id
- state.client_secret = data.client_secret
+ setClientData (state, { clientId, clientSecret }) {
+ state.clientId = clientId
+ state.clientSecret = clientSecret
+ },
+ setAppToken (state, token) {
+ state.appToken = token
},
setToken (state, token) {
- state.token = token
+ state.userToken = token
+ },
+ clearToken (state) {
+ state.userToken = false
+ // state.token is userToken with older name, coming from persistent state
+ // let's clear it as well, since it is being used as a fallback of state.userToken
+ del(state, 'token')
+ }
+ },
+ getters: {
+ getToken: state => () => {
+ // state.token is userToken with older name, coming from persistent state
+ // added here for smoother transition, otherwise user will be logged out
+ return state.userToken || state.token || state.appToken
+ },
+ getUserToken: state => () => {
+ // state.token is userToken with older name, coming from persistent state
+ // added here for smoother transition, otherwise user will be logged out
+ return state.userToken || state.token
}
}
}
diff --git a/src/modules/oauth_tokens.js b/src/modules/oauth_tokens.js
index 00ac1431..0159a3f1 100644
--- a/src/modules/oauth_tokens.js
+++ b/src/modules/oauth_tokens.js
@@ -3,12 +3,12 @@ const oauthTokens = {
tokens: []
},
actions: {
- fetchTokens ({rootState, commit}) {
+ fetchTokens ({ rootState, commit }) {
rootState.api.backendInteractor.fetchOAuthTokens().then((tokens) => {
commit('swapTokens', tokens)
})
},
- revokeToken ({rootState, commit, state}, id) {
+ revokeToken ({ rootState, commit, state }, id) {
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
new file mode 100644
index 00000000..e6158b63
--- /dev/null
+++ b/src/modules/polls.js
@@ -0,0 +1,70 @@
+import { merge } from 'lodash'
+import { set } from 'vue'
+
+const polls = {
+ state: {
+ // Contains key = id, value = number of trackers for this poll
+ trackedPolls: {},
+ pollsObject: {}
+ },
+ mutations: {
+ mergeOrAddPoll (state, poll) {
+ const existingPoll = state.pollsObject[poll.id]
+ // Make expired-state change trigger re-renders properly
+ poll.expired = Date.now() > Date.parse(poll.expires_at)
+ if (existingPoll) {
+ set(state.pollsObject, poll.id, merge(existingPoll, poll))
+ } else {
+ set(state.pollsObject, poll.id, poll)
+ }
+ },
+ trackPoll (state, pollId) {
+ const currentValue = state.trackedPolls[pollId]
+ if (currentValue) {
+ set(state.trackedPolls, pollId, currentValue + 1)
+ } else {
+ set(state.trackedPolls, pollId, 1)
+ }
+ },
+ untrackPoll (state, pollId) {
+ const currentValue = state.trackedPolls[pollId]
+ if (currentValue) {
+ set(state.trackedPolls, pollId, currentValue - 1)
+ } else {
+ set(state.trackedPolls, pollId, 0)
+ }
+ }
+ },
+ actions: {
+ mergeOrAddPoll ({ commit }, poll) {
+ commit('mergeOrAddPoll', poll)
+ },
+ updateTrackedPoll ({ rootState, dispatch, commit }, pollId) {
+ rootState.api.backendInteractor.fetchPoll(pollId).then(poll => {
+ setTimeout(() => {
+ if (rootState.polls.trackedPolls[pollId]) {
+ dispatch('updateTrackedPoll', pollId)
+ }
+ }, 30 * 1000)
+ commit('mergeOrAddPoll', poll)
+ })
+ },
+ trackPoll ({ rootState, commit, dispatch }, pollId) {
+ if (!rootState.polls.trackedPolls[pollId]) {
+ setTimeout(() => dispatch('updateTrackedPoll', pollId), 30 * 1000)
+ }
+ commit('trackPoll', pollId)
+ },
+ untrackPoll ({ commit }, pollId) {
+ commit('untrackPoll', pollId)
+ },
+ votePoll ({ rootState, commit }, { id, pollId, choices }) {
+ return rootState.api.backendInteractor.vote(pollId, choices).then(poll => {
+ commit('mergeOrAddPoll', poll)
+ return poll
+ })
+ }
+ }
+}
+
+export default polls
diff --git a/src/modules/reports.js b/src/modules/reports.js
new file mode 100644
index 00000000..904022f1
--- /dev/null
+++ b/src/modules/reports.js
@@ -0,0 +1,30 @@
+import filter from 'lodash/filter'
+
+const reports = {
+ state: {
+ userId: null,
+ statuses: [],
+ modalActivated: false
+ },
+ mutations: {
+ openUserReportingModal (state, { userId, statuses }) {
+ state.userId = userId
+ state.statuses = statuses
+ state.modalActivated = true
+ },
+ closeUserReportingModal (state) {
+ state.modalActivated = false
+ }
+ },
+ actions: {
+ openUserReportingModal ({ rootState, commit }, userId) {
+ const statuses = filter(rootState.statuses.allStatuses, status => status.user.id === userId)
+ commit('openUserReportingModal', { userId, statuses })
+ },
+ closeUserReportingModal ({ commit }) {
+ commit('closeUserReportingModal')
+ }
+ }
+}
+
+export default reports
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 8e0203e3..7d5d5a67 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -1,4 +1,4 @@
-import { remove, slice, each, 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 apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.js'
@@ -20,20 +20,22 @@ const emptyTl = (userId = 0) => ({
flushMarker: 0
})
+const emptyNotifications = () => ({
+ desktopNotificationSilence: true,
+ maxId: 0,
+ minId: Number.POSITIVE_INFINITY,
+ data: [],
+ idStore: {},
+ loading: false,
+ error: false
+})
+
export const defaultState = () => ({
allStatuses: [],
allStatusesObject: {},
+ conversationsObject: {},
maxId: 0,
- notifications: {
- desktopNotificationSilence: true,
- maxId: 0,
- minId: Number.POSITIVE_INFINITY,
- data: [],
- idStore: {},
- loading: false,
- error: false,
- fetcherId: null
- },
+ notifications: emptyNotifications(),
favorites: new Set(),
error: false,
timelines: {
@@ -78,13 +80,13 @@ const mergeOrAdd = (arr, obj, item) => {
merge(oldItem, omitBy(item, (v, k) => v === null || k === 'user'))
// Reactivity fix.
oldItem.attachments.splice(oldItem.attachments.length)
- return {item: oldItem, new: false}
+ return { item: oldItem, new: false }
} else {
// This is a new item, prepare it
prepareStatus(item)
arr.push(item)
set(obj, item.id, item)
- return {item, new: true}
+ return { item, new: true }
}
}
@@ -111,14 +113,47 @@ const sortTimeline = (timeline) => {
return timeline
}
-const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId }) => {
+// Add status to the global storages (arrays and objects maintaining statuses) except timelines
+const addStatusToGlobalStorage = (state, data) => {
+ const result = mergeOrAdd(state.allStatuses, state.allStatusesObject, data)
+ if (result.new) {
+ // Add to conversation
+ const status = result.item
+ const conversationsObject = state.conversationsObject
+ const conversationId = status.statusnet_conversation_id
+ if (conversationsObject[conversationId]) {
+ conversationsObject[conversationId].push(status)
+ } else {
+ set(conversationsObject, conversationId, [status])
+ }
+ }
+ return result
+}
+
+// Remove status from the global storages (arrays and objects maintaining statuses) except timelines
+const removeStatusFromGlobalStorage = (state, status) => {
+ remove(state.allStatuses, { id: status.id })
+
+ // TODO: Need to remove from allStatusesObject?
+
+ // Remove possible notification
+ remove(state.notifications.data, ({ action: { id } }) => id === status.id)
+
+ // Remove from conversation
+ const conversationId = status.statusnet_conversation_id
+ if (state.conversationsObject[conversationId]) {
+ remove(state.conversationsObject[conversationId], { id: status.id })
+ }
+}
+
+const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {},
+ noIdUpdate = false, userId }) => {
// Sanity check
if (!isArray(statuses)) {
return false
}
const allStatuses = state.allStatuses
- const allStatusesObject = state.allStatusesObject
const timelineObject = state.timelines[timeline]
const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0
@@ -141,7 +176,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
}
const addStatus = (data, showImmediately, addToTimeline = true) => {
- const result = mergeOrAdd(allStatuses, allStatusesObject, data)
+ const result = addStatusToGlobalStorage(state, data)
const status = result.item
if (result.new) {
@@ -235,16 +270,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
},
'deletion': (deletion) => {
const uri = deletion.uri
-
- // Remove possible notification
- const status = find(allStatuses, {uri})
+ const status = find(allStatuses, { uri })
if (!status) {
return
}
- remove(state.notifications.data, ({action: {id}}) => id === status.id)
+ removeStatusFromGlobalStorage(state, status)
- remove(allStatuses, { uri })
if (timeline) {
remove(timelineObject.statuses, { uri })
remove(timelineObject.visibleStatuses, { uri })
@@ -271,12 +303,12 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
}
}
-const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes }) => {
- const allStatuses = state.allStatuses
- const allStatusesObject = state.allStatusesObject
+const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
each(notifications, (notification) => {
- notification.action = mergeOrAdd(allStatuses, allStatusesObject, notification.action).item
- notification.status = notification.status && mergeOrAdd(allStatuses, allStatusesObject, notification.status).item
+ if (notification.type !== 'follow') {
+ notification.action = addStatusToGlobalStorage(state, notification.action).item
+ notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
+ }
// Only add a new notification if we don't have one for the same action
if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
@@ -292,15 +324,32 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
if ('Notification' in window && window.Notification.permission === 'granted') {
const notifObj = {}
- const action = notification.action
- const title = action.user.name
- notifObj.icon = action.user.profile_image_url
- notifObj.body = action.text // there's a problem that it doesn't put a space before links tho
+ const status = notification.status
+ const title = notification.from_profile.name
+ notifObj.icon = notification.from_profile.profile_image_url
+ let i18nString
+ switch (notification.type) {
+ case 'like':
+ i18nString = 'favorited_you'
+ break
+ case 'repeat':
+ i18nString = 'repeated_you'
+ break
+ case 'follow':
+ i18nString = 'followed_you'
+ break
+ }
+
+ if (i18nString) {
+ notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
+ } else {
+ notifObj.body = notification.status.text
+ }
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
- if (action.attachments && action.attachments.length > 0 && !action.nsfw &&
- action.attachments[0].mimetype.startsWith('image/')) {
- notifObj.image = action.attachments[0].url
+ if (status && status.attachments && status.attachments.length > 0 && !status.nsfw &&
+ status.attachments[0].mimetype.startsWith('image/')) {
+ notifObj.image = status.attachments[0].url
}
if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) {
@@ -340,26 +389,46 @@ export const mutations = {
oldTimeline.visibleStatusesObject = {}
each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
},
- setNotificationFetcher (state, { fetcherId }) {
- state.notifications.fetcherId = fetcherId
- },
resetStatuses (state) {
const emptyState = defaultState()
Object.entries(emptyState).forEach(([key, value]) => {
state[key] = value
})
},
- clearTimeline (state, { timeline }) {
- state.timelines[timeline] = emptyTl(state.timelines[timeline].userId)
+ clearTimeline (state, { timeline, excludeUserId = false }) {
+ const userId = excludeUserId ? state.timelines[timeline].userId : undefined
+ state.timelines[timeline] = emptyTl(userId)
+ },
+ clearNotifications (state) {
+ state.notifications = emptyNotifications()
},
setFavorited (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
+
+ if (newStatus.favorited !== value) {
+ if (value) {
+ newStatus.fave_num++
+ } else {
+ newStatus.fave_num--
+ }
+ }
+
newStatus.favorited = value
},
- setFavoritedConfirm (state, { status }) {
+ setFavoritedConfirm (state, { status, user }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.favorited = status.favorited
newStatus.fave_num = status.fave_num
+ const index = findIndex(newStatus.favoritedBy, { id: user.id })
+ if (index !== -1 && !newStatus.favorited) {
+ newStatus.favoritedBy.splice(index, 1)
+ } else if (index === -1 && newStatus.favorited) {
+ newStatus.favoritedBy.push(user)
+ }
+ },
+ setPinned (state, status) {
+ const newStatus = state.allStatusesObject[status.id]
+ newStatus.pinned = status.pinned
},
setRetweeted (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
@@ -374,10 +443,28 @@ export const mutations = {
newStatus.repeated = value
},
+ setRetweetedConfirm (state, { status, user }) {
+ const newStatus = state.allStatusesObject[status.id]
+ newStatus.repeated = status.repeated
+ newStatus.repeat_num = status.repeat_num
+ const index = findIndex(newStatus.rebloggedBy, { id: user.id })
+ if (index !== -1 && !newStatus.repeated) {
+ newStatus.rebloggedBy.splice(index, 1)
+ } else if (index === -1 && newStatus.repeated) {
+ newStatus.rebloggedBy.push(user)
+ }
+ },
setDeleted (state, { status }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.deleted = true
},
+ setManyDeleted (state, condition) {
+ Object.values(state.allStatusesObject).forEach(status => {
+ if (condition(status)) {
+ status.deleted = true
+ }
+ })
+ },
setLoading (state, { timeline, value }) {
state.timelines[timeline].loading = value
},
@@ -404,6 +491,24 @@ export const mutations = {
},
queueFlush (state, { timeline, id }) {
state.timelines[timeline].flushMarker = id
+ },
+ addRepeats (state, { id, rebloggedByUsers, currentUser }) {
+ const newStatus = state.allStatusesObject[id]
+ newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _)
+ // repeats stats can be incorrect based on polling condition, let's update them using the most recent data
+ newStatus.repeat_num = newStatus.rebloggedBy.length
+ newStatus.repeated = !!newStatus.rebloggedBy.find(({ id }) => currentUser.id === id)
+ },
+ addFavs (state, { id, favoritedByUsers, currentUser }) {
+ const newStatus = state.allStatusesObject[id]
+ newStatus.favoritedBy = favoritedByUsers.filter(_ => _)
+ // favorites stats can be incorrect based on polling condition, let's update them using the most recent data
+ newStatus.fave_num = newStatus.favoritedBy.length
+ newStatus.favorited = !!newStatus.favoritedBy.find(({ id }) => currentUser.id === id)
+ },
+ updateStatusWithPoll (state, { id, poll }) {
+ const status = state.allStatusesObject[id]
+ status.poll = poll
}
}
@@ -413,8 +518,8 @@ const statuses = {
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId }) {
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId })
},
- addNewNotifications ({ rootState, commit, dispatch }, { notifications, older }) {
- commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older })
+ addNewNotifications ({ rootState, commit, dispatch, rootGetters }, { notifications, older }) {
+ commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters })
},
setError ({ rootState, commit }, { value }) {
commit('setError', { value })
@@ -428,40 +533,48 @@ const statuses = {
setNotificationsSilence ({ rootState, commit }, { value }) {
commit('setNotificationsSilence', { value })
},
- stopFetchingNotifications ({ rootState, commit }) {
- if (rootState.statuses.notifications.fetcherId) {
- window.clearInterval(rootState.statuses.notifications.fetcherId)
- }
- commit('setNotificationFetcher', { fetcherId: null })
- },
deleteStatus ({ rootState, commit }, status) {
commit('setDeleted', { status })
apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
},
+ markStatusesAsDeleted ({ commit }, condition) {
+ commit('setManyDeleted', condition)
+ },
favorite ({ rootState, commit }, status) {
// Optimistic favoriting...
commit('setFavorited', { status, value: true })
- apiService.favorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
- .then(status => {
- commit('setFavoritedConfirm', { status })
- })
+ rootState.api.backendInteractor.favorite(status.id)
+ .then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
},
unfavorite ({ rootState, commit }, status) {
- // Optimistic favoriting...
+ // Optimistic unfavoriting...
commit('setFavorited', { status, value: false })
- apiService.unfavorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
- .then(status => {
- commit('setFavoritedConfirm', { status })
- })
+ rootState.api.backendInteractor.unfavorite(status.id)
+ .then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
+ },
+ fetchPinnedStatuses ({ rootState, dispatch }, userId) {
+ rootState.api.backendInteractor.fetchPinnedStatuses(userId)
+ .then(statuses => dispatch('addNewStatuses', { statuses, timeline: 'user', userId, showImmediately: true, noIdUpdate: true }))
+ },
+ pinStatus ({ rootState, commit }, statusId) {
+ return rootState.api.backendInteractor.pinOwnStatus(statusId)
+ .then((status) => commit('setPinned', status))
+ },
+ unpinStatus ({ rootState, commit }, statusId) {
+ rootState.api.backendInteractor.unpinOwnStatus(statusId)
+ .then((status) => commit('setPinned', status))
},
retweet ({ rootState, commit }, status) {
// Optimistic retweeting...
commit('setRetweeted', { status, value: true })
- apiService.retweet({ id: status.id, credentials: rootState.users.currentUser.credentials })
+ rootState.api.backendInteractor.retweet(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 })
- apiService.unretweet({ id: status.id, credentials: rootState.users.currentUser.credentials })
+ rootState.api.backendInteractor.unretweet(status.id)
+ .then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser }))
},
queueFlush ({ rootState, commit }, { timeline, id }) {
commit('queueFlush', { timeline, id })
@@ -472,6 +585,31 @@ const statuses = {
id: rootState.statuses.notifications.maxId,
credentials: rootState.users.currentUser.credentials
})
+ },
+ fetchFavsAndRepeats ({ rootState, commit }, id) {
+ Promise.all([
+ 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)
+ .then(favoritedByUsers => commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser }))
+ },
+ fetchRepeats ({ rootState, commit }, id) {
+ rootState.api.backendInteractor.fetchRebloggedByUsers(id)
+ .then(rebloggedByUsers => commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser }))
+ },
+ search (store, { q, resolve, limit, offset, following }) {
+ return store.rootState.api.backendInteractor.search2({ q, resolve, limit, offset, following })
+ .then((data) => {
+ store.commit('addNewUsers', data.accounts)
+ store.commit('addNewStatuses', { statuses: data.statuses })
+ return data
+ })
}
},
mutations
diff --git a/src/modules/users.js b/src/modules/users.js
index 1a507d31..57d3a3e3 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -1,8 +1,8 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
-import { compact, map, each, merge, find, last } from 'lodash'
+import oauthApi from '../services/new_api/oauth.js'
+import { compact, map, each, merge, last, concat, uniq } from 'lodash'
import { set } from 'vue'
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
-import oauthApi from '../services/new_api/oauth'
import { humanizeErrors } from './errors'
// TODO: Unify with mergeOrAdd in statuses.js
@@ -32,11 +32,62 @@ const getNotificationPermission = () => {
return Promise.resolve(Notification.permission)
}
+const blockUser = (store, id) => {
+ return store.rootState.api.backendInteractor.blockUser(id)
+ .then((relationship) => {
+ store.commit('updateUserRelationship', [relationship])
+ store.commit('addBlockId', id)
+ store.commit('removeStatus', { timeline: 'friends', userId: id })
+ store.commit('removeStatus', { timeline: 'public', userId: id })
+ store.commit('removeStatus', { timeline: 'publicAndExternal', userId: id })
+ })
+}
+
+const unblockUser = (store, 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)
+ .then((relationship) => {
+ store.commit('updateUserRelationship', [relationship])
+ store.commit('addMuteId', id)
+ })
+}
+
+const unmuteUser = (store, id) => {
+ return store.rootState.api.backendInteractor.unmuteUser(id)
+ .then((relationship) => store.commit('updateUserRelationship', [relationship]))
+}
+
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 || []
+ const newTags = tags.concat([tag])
+ set(user, 'tags', newTags)
+ },
+ untagUser (state, { user: { id }, tag }) {
+ const user = state.usersObject[id]
+ const tags = user.tags || []
+ const newTags = tags.filter(t => t !== tag)
+ set(user, 'tags', newTags)
+ },
+ updateRight (state, { user: { id }, right, value }) {
+ const user = state.usersObject[id]
+ let newRights = user.rights
+ newRights[right] = value
+ set(user, 'rights', newRights)
+ },
+ updateActivationStatus (state, { user: { id }, status }) {
+ const user = state.usersObject[id]
+ set(user, 'deactivated', !status)
+ },
setCurrentUser (state, user) {
state.lastLoginName = user.screen_name
state.currentUser = merge(state.currentUser || {}, user)
@@ -51,42 +102,27 @@ export const mutations = {
endLogin (state) {
state.loggingIn = false
},
- // TODO Clean after ourselves?
- addFriends (state, { id, friends }) {
+ saveFriendIds (state, { id, friendIds }) {
const user = state.usersObject[id]
- each(friends, friend => {
- if (!find(user.friends, { id: friend.id })) {
- user.friends.push(friend)
- }
- })
- user.lastFriendId = last(friends).id
+ user.friendIds = uniq(concat(user.friendIds, friendIds))
},
- addFollowers (state, { id, followers }) {
+ saveFollowerIds (state, { id, followerIds }) {
const user = state.usersObject[id]
- each(followers, follower => {
- if (!find(user.followers, { id: follower.id })) {
- user.followers.push(follower)
- }
- })
- user.lastFollowerId = last(followers).id
+ user.followerIds = uniq(concat(user.followerIds, followerIds))
},
// Because frontend doesn't have a reason to keep these stuff in memory
// outside of viewing someones user profile.
clearFriends (state, userId) {
const user = state.usersObject[userId]
- if (!user) {
- return
+ if (user) {
+ set(user, 'friendIds', [])
}
- user.friends = []
- user.lastFriendId = null
},
clearFollowers (state, userId) {
const user = state.usersObject[userId]
- if (!user) {
- return
+ if (user) {
+ set(user, 'followerIds', [])
}
- user.followers = []
- user.lastFollowerId = null
},
addNewUsers (state, users) {
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
@@ -99,6 +135,7 @@ export const mutations = {
user.following = relationship.following
user.muted = relationship.muting
user.statusnet_blocking = relationship.blocking
+ user.subscribed = relationship.subscribing
}
})
},
@@ -110,6 +147,11 @@ export const mutations = {
saveBlockIds (state, blockIds) {
state.currentUser.blockIds = blockIds
},
+ addBlockId (state, blockId) {
+ if (state.currentUser.blockIds.indexOf(blockId) === -1) {
+ state.currentUser.blockIds.push(blockId)
+ }
+ },
updateMutes (state, mutedUsers) {
// Reset muted of all fetched users
each(state.users, (user) => { user.muted = false })
@@ -118,12 +160,28 @@ export const mutations = {
saveMuteIds (state, muteIds) {
state.currentUser.muteIds = muteIds
},
+ addMuteId (state, muteId) {
+ if (state.currentUser.muteIds.indexOf(muteId) === -1) {
+ state.currentUser.muteIds.push(muteId)
+ }
+ },
+ setPinned (state, status) {
+ const user = state.usersObject[status.user.id]
+ const index = user.pinnedStatuseIds.indexOf(status.id)
+ if (status.pinned && index === -1) {
+ user.pinnedStatuseIds.push(status.id)
+ } else if (!status.pinned && index !== -1) {
+ user.pinnedStatuseIds.splice(index, 1)
+ }
+ },
setUserForStatus (state, status) {
status.user = state.usersObject[status.user.id]
},
setUserForNotification (state, notification) {
- notification.action.user = state.usersObject[notification.action.user.id]
- notification.from_profile = state.usersObject[notification.action.user.id]
+ if (notification.type !== 'follow') {
+ notification.action.user = state.usersObject[notification.action.user.id]
+ }
+ notification.from_profile = state.usersObject[notification.from_profile.id]
},
setColor (state, { user: { id }, highlighted }) {
const user = state.usersObject[id]
@@ -176,8 +234,10 @@ const users = {
})
},
fetchUserRelationship (store, id) {
- return store.rootState.api.backendInteractor.fetchUserRelationship({ id })
- .then((relationships) => store.commit('updateUserRelationship', relationships))
+ if (store.state.currentUser) {
+ store.rootState.api.backendInteractor.fetchUserRelationship({ id })
+ .then((relationships) => store.commit('updateUserRelationship', relationships))
+ }
},
fetchBlocks (store) {
return store.rootState.api.backendInteractor.fetchBlocks()
@@ -187,18 +247,17 @@ const users = {
return blocks
})
},
- blockUser (store, userId) {
- return store.rootState.api.backendInteractor.blockUser(userId)
- .then((relationship) => {
- store.commit('updateUserRelationship', [relationship])
- store.commit('removeStatus', { timeline: 'friends', userId })
- store.commit('removeStatus', { timeline: 'public', userId })
- store.commit('removeStatus', { timeline: 'publicAndExternal', userId })
- })
+ blockUser (store, id) {
+ return blockUser(store, id)
},
unblockUser (store, id) {
- return store.rootState.api.backendInteractor.unblockUser(id)
- .then((relationship) => store.commit('updateUserRelationship', [relationship]))
+ return unblockUser(store, id)
+ },
+ blockUsers (store, ids = []) {
+ return Promise.all(ids.map(id => blockUser(store, id)))
+ },
+ unblockUsers (store, ids = []) {
+ return Promise.all(ids.map(id => unblockUser(store, id)))
},
fetchMutes (store) {
return store.rootState.api.backendInteractor.fetchMutes()
@@ -209,32 +268,34 @@ const users = {
})
},
muteUser (store, id) {
- return store.rootState.api.backendInteractor.muteUser(id)
- .then((relationship) => store.commit('updateUserRelationship', [relationship]))
+ return muteUser(store, id)
},
unmuteUser (store, id) {
- return store.rootState.api.backendInteractor.unmuteUser(id)
- .then((relationship) => store.commit('updateUserRelationship', [relationship]))
+ return unmuteUser(store, id)
},
- addFriends ({ rootState, commit }, fetchBy) {
- return new Promise((resolve, reject) => {
- const user = rootState.users.usersObject[fetchBy]
- const maxId = user.lastFriendId
- rootState.api.backendInteractor.fetchFriends({ id: user.id, maxId })
- .then((friends) => {
- commit('addFriends', { id: user.id, friends })
- resolve(friends)
- }).catch(() => {
- reject()
- })
- })
+ muteUsers (store, ids = []) {
+ return Promise.all(ids.map(id => muteUser(store, id)))
+ },
+ unmuteUsers (store, ids = []) {
+ return Promise.all(ids.map(id => unmuteUser(store, id)))
+ },
+ fetchFriends ({ rootState, commit }, id) {
+ const user = rootState.users.usersObject[id]
+ const maxId = last(user.friendIds)
+ return rootState.api.backendInteractor.fetchFriends({ id, maxId })
+ .then((friends) => {
+ commit('addNewUsers', friends)
+ commit('saveFriendIds', { id, friendIds: map(friends, 'id') })
+ return friends
+ })
},
- addFollowers ({ rootState, commit }, fetchBy) {
- const user = rootState.users.usersObject[fetchBy]
- const maxId = user.lastFollowerId
- return rootState.api.backendInteractor.fetchFollowers({ id: user.id, maxId })
+ fetchFollowers ({ rootState, commit }, id) {
+ const user = rootState.users.usersObject[id]
+ const maxId = last(user.followerIds)
+ return rootState.api.backendInteractor.fetchFollowers({ id, maxId })
.then((followers) => {
- commit('addFollowers', { id: user.id, followers })
+ commit('addNewUsers', followers)
+ commit('saveFollowerIds', { id, followerIds: map(followers, 'id') })
return followers
})
},
@@ -244,6 +305,14 @@ const users = {
clearFollowers ({ commit }, userId) {
commit('clearFollowers', userId)
},
+ subscribeUser ({ rootState, commit }, id) {
+ return rootState.api.backendInteractor.subscribeUser(id)
+ .then((relationship) => commit('updateUserRelationship', [relationship]))
+ },
+ unsubscribeUser ({ rootState, commit }, id) {
+ return rootState.api.backendInteractor.unsubscribeUser(id)
+ .then((relationship) => commit('updateUserRelationship', [relationship]))
+ },
registerPushNotifications (store) {
const token = store.state.currentUser.credentials
const vapidPublicKey = store.rootState.instance.vapidPublicKey
@@ -257,19 +326,26 @@ const users = {
unregisterPushNotifications(token)
},
+ addNewUsers ({ commit }, users) {
+ commit('addNewUsers', users)
+ },
addNewStatuses (store, { statuses }) {
const users = map(statuses, 'user')
const retweetedUsers = compact(map(statuses, 'retweeted_status.user'))
store.commit('addNewUsers', users)
store.commit('addNewUsers', retweetedUsers)
- // Reconnect users to statuses
each(statuses, (status) => {
+ // Reconnect users to statuses
store.commit('setUserForStatus', status)
+ // Set pinned statuses to user
+ store.commit('setPinned', status)
})
- // Reconnect users to retweets
each(compact(map(statuses, 'retweeted_status')), (status) => {
+ // Reconnect users to retweets
store.commit('setUserForStatus', status)
+ // Set pinned retweets to user
+ store.commit('setPinned', status)
})
},
addNewNotifications (store, { notifications }) {
@@ -279,60 +355,78 @@ const users = {
const notificationsObject = store.rootState.statuses.notifications.idStore
const relevantNotifications = Object.entries(notificationsObject)
- .filter(([k, val]) => notificationIds.includes(k))
- .map(([k, val]) => val)
+ .filter(([k, val]) => notificationIds.includes(k))
+ .map(([k, val]) => val)
// Reconnect users to notifications
each(relevantNotifications, (notification) => {
store.commit('setUserForNotification', notification)
})
},
+ searchUsers (store, query) {
+ return store.rootState.api.backendInteractor.searchUsers(query)
+ .then((users) => {
+ store.commit('addNewUsers', users)
+ return users
+ })
+ },
async signUp (store, userInfo) {
store.commit('signUpPending')
let rootState = store.rootState
- let response = await rootState.api.backendInteractor.register(userInfo)
- if (response.ok) {
- const data = {
- oauth: rootState.oauth,
- instance: rootState.instance.server
- }
- let app = await oauthApi.getOrCreateApp(data)
- let result = await oauthApi.getTokenWithCredentials({
- app,
- instance: data.instance,
- username: userInfo.username,
- password: userInfo.password
- })
+ try {
+ let data = await rootState.api.backendInteractor.register(userInfo)
store.commit('signUpSuccess')
- store.commit('setToken', result.access_token)
- store.dispatch('loginUser', result.access_token)
- } else {
- const data = await response.json()
- let errors = JSON.parse(data.error)
+ store.commit('setToken', data.access_token)
+ store.dispatch('loginUser', data.access_token)
+ } catch (e) {
+ let errors = e.message
// replace ap_id with username
- if (errors.ap_id) {
- errors.username = errors.ap_id
- delete errors.ap_id
+ if (typeof errors === 'object') {
+ if (errors.ap_id) {
+ errors.username = errors.ap_id
+ delete errors.ap_id
+ }
+ errors = humanizeErrors(errors)
}
- errors = humanizeErrors(errors)
store.commit('signUpFailure', errors)
throw Error(errors)
}
},
async getCaptcha (store) {
- return await store.rootState.api.backendInteractor.getCaptcha()
+ return store.rootState.api.backendInteractor.getCaptcha()
},
logout (store) {
- store.commit('clearCurrentUser')
- store.dispatch('disconnectFromChat')
- store.commit('setToken', false)
- store.dispatch('stopFetching', 'friends')
- store.commit('setBackendInteractor', backendInteractorService())
- store.dispatch('stopFetchingNotifications')
- store.commit('resetStatuses')
+ const { oauth, instance } = store.rootState
+
+ const data = {
+ ...oauth,
+ commit: store.commit,
+ instance: instance.server
+ }
+
+ return oauthApi.getOrCreateApp(data)
+ .then((app) => {
+ const params = {
+ app,
+ instance: data.instance,
+ token: oauth.userToken
+ }
+
+ return oauthApi.revokeToken(params)
+ })
+ .then(() => {
+ store.commit('clearCurrentUser')
+ store.dispatch('disconnectFromChat')
+ store.commit('clearToken')
+ store.dispatch('stopFetching', 'friends')
+ store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
+ store.dispatch('stopFetching', 'notifications')
+ store.commit('clearNotifications')
+ store.commit('resetStatuses')
+ })
},
loginUser (store, accessToken) {
return new Promise((resolve, reject) => {
@@ -363,7 +457,10 @@ const users = {
}
// Start getting fresh posts.
- store.dispatch('startFetching', { timeline: 'friends' })
+ store.dispatch('startFetchingTimeline', { timeline: 'friends' })
+
+ // Start fetching notifications
+ store.dispatch('startFetchingNotifications')
// Get user mutes
store.dispatch('fetchMutes')
@@ -376,19 +473,19 @@ const users = {
// Authentication failed
commit('endLogin')
if (response.status === 401) {
- reject('Wrong username or password')
+ reject(new Error('Wrong username or password'))
} else {
- reject('An error occurred, please try again')
+ reject(new Error('An error occurred, please try again'))
}
}
commit('endLogin')
resolve()
})
- .catch((error) => {
- console.log(error)
- commit('endLogin')
- reject('Failed to connect to server, try again')
- })
+ .catch((error) => {
+ console.log(error)
+ commit('endLogin')
+ reject(new Error('Failed to connect to server, try again'))
+ })
})
}
}