diff options
Diffstat (limited to 'src/modules')
| -rw-r--r-- | src/modules/api.js | 27 | ||||
| -rw-r--r-- | src/modules/auth_flow.js | 89 | ||||
| -rw-r--r-- | src/modules/config.js | 5 | ||||
| -rw-r--r-- | src/modules/instance.js | 4 | ||||
| -rw-r--r-- | src/modules/interface.js | 9 | ||||
| -rw-r--r-- | src/modules/oauth.js | 37 | ||||
| -rw-r--r-- | src/modules/reports.js | 30 | ||||
| -rw-r--r-- | src/modules/statuses.js | 212 | ||||
| -rw-r--r-- | src/modules/users.js | 243 |
9 files changed, 495 insertions, 161 deletions
diff --git a/src/modules/api.js b/src/modules/api.js index 31cb55c6..7ed3edac 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) diff --git a/src/modules/auth_flow.js b/src/modules/auth_flow.js new file mode 100644 index 00000000..86328cf3 --- /dev/null +++ b/src/modules/auth_flow.js @@ -0,0 +1,89 @@ +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 = { + 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/config.js b/src/modules/config.js index c5491c01..a8da525a 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,10 +31,12 @@ const defaultState = { muteWords: [], highlight: {}, interfaceLanguage: browserLocale, + hideScopeNotice: false, scopeCopy: undefined, // instance default subjectLineBehavior: undefined, // instance default alwaysShowSubjectInput: undefined, // instance default - postContentType: undefined // instance default + postContentType: undefined, // instance default + minimalScopesMode: undefined // instance default } const config = { diff --git a/src/modules/instance.js b/src/modules/instance.js index f778ac4d..fc4578ed 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -5,6 +5,7 @@ const defaultState = { // Stuff from static/config.json and apiConfig name: 'Pleroma FE', registrationOpen: true, + safeDM: true, textlimit: 5000, server: 'http://localhost:4040/', theme: 'pleroma-dark', @@ -15,7 +16,6 @@ const defaultState = { redirectRootNoLogin: '/main/all', redirectRootLogin: '/main/friends', showInstanceSpecificPanel: false, - scopeOptionsEnabled: true, formattingOptionsEnabled: false, alwaysShowSubjectInput: true, hideMutedPosts: false, @@ -27,11 +27,11 @@ const defaultState = { scopeCopy: true, subjectLineBehavior: 'email', postContentType: 'text/plain', - loginMethod: 'password', nsfwCensorImage: undefined, vapidPublicKey: undefined, noAttachmentLinks: false, showFeaturesPanel: true, + minimalScopesMode: false, // Nasty stuff pleromaBackend: true, diff --git a/src/modules/interface.js b/src/modules/interface.js index 956c9cb3..5b2762e5 100644 --- a/src/modules/interface.js +++ b/src/modules/interface.js @@ -11,7 +11,8 @@ const defaultState = { window.CSS.supports('filter', 'drop-shadow(0 0)') || window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)') ) - } + }, + mobileLayout: false } const interfaceMod = { @@ -31,6 +32,9 @@ const interfaceMod = { }, setNotificationPermission (state, permission) { state.notificationPermission = permission + }, + setMobileLayout (state, value) { + state.mobileLayout = value } }, actions: { @@ -42,6 +46,9 @@ const interfaceMod = { }, setNotificationPermission ({ commit }, permission) { commit('setNotificationPermission', permission) + }, + setMobileLayout ({ commit }, value) { + commit('setMobileLayout', value) } } } diff --git a/src/modules/oauth.js b/src/modules/oauth.js index 144ff830..11cb10fe 100644 --- a/src/modules/oauth.js +++ b/src/modules/oauth.js @@ -1,16 +1,39 @@ 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 + } + }, + 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/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 742eecba..e6ee5447 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: { @@ -111,6 +113,39 @@ const sortTimeline = (timeline) => { return timeline } +// 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)) { @@ -118,12 +153,11 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us } const allStatuses = state.allStatuses - const allStatusesObject = state.allStatusesObject const timelineObject = state.timelines[timeline] const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0 const minNew = statuses.length > 0 ? minBy(statuses, 'id').id : 0 - const newer = timeline && maxNew > timelineObject.maxId && statuses.length > 0 + const newer = timeline && (maxNew > timelineObject.maxId || timelineObject.maxId === 0) && statuses.length > 0 const older = timeline && (minNew < timelineObject.minId || timelineObject.minId === 0) && statuses.length > 0 if (!noIdUpdate && newer) { @@ -141,7 +175,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 +269,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us }, 'deletion': (deletion) => { const uri = deletion.uri - - // Remove possible notification 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 +302,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 +323,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,9 +388,6 @@ 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]) => { @@ -352,14 +397,36 @@ export const mutations = { clearTimeline (state, { timeline }) { state.timelines[timeline] = emptyTl(state.timelines[timeline].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 +441,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 +489,11 @@ export const mutations = { }, queueFlush (state, { timeline, id }) { state.timelines[timeline].flushMarker = id + }, + addFavsAndRepeats (state, { id, favoritedByUsers, rebloggedByUsers }) { + const newStatus = state.allStatusesObject[id] + newStatus.favoritedBy = favoritedByUsers.filter(_ => _) + newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _) } } @@ -413,8 +503,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 +518,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 })) + }, + 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 +570,14 @@ 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('addFavsAndRepeats', { id, favoritedByUsers, rebloggedByUsers }) + ) } }, mutations diff --git a/src/modules/users.js b/src/modules/users.js index 1a507d31..739b8b92 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 userSearchApi from '../services/new_api/user_search.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)) @@ -110,6 +146,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 +159,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 +233,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 +246,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 +267,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))) }, - addFollowers ({ rootState, commit }, fetchBy) { - const user = rootState.users.usersObject[fetchBy] - const maxId = user.lastFollowerId - return rootState.api.backendInteractor.fetchFollowers({ id: user.id, maxId }) + 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 + }) + }, + 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 }) }, @@ -257,19 +317,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 }) { @@ -287,36 +354,34 @@ const users = { store.commit('setUserForNotification', notification) }) }, + searchUsers (store, query) { + // TODO: Move userSearch api into api.service + return userSearchApi.search({query, store: { state: store.rootState }}) + .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) } @@ -330,8 +395,9 @@ const users = { store.dispatch('disconnectFromChat') store.commit('setToken', false) store.dispatch('stopFetching', 'friends') - store.commit('setBackendInteractor', backendInteractorService()) - store.dispatch('stopFetchingNotifications') + store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) + store.dispatch('stopFetching', 'notifications') + store.commit('clearNotifications') store.commit('resetStatuses') }, loginUser (store, accessToken) { @@ -363,7 +429,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') |
