Commit 5c967651 authored by Shpuld Shpludson's avatar Shpuld Shpludson

Merge branch 'feat/followers-following-lists' into 'master'

Feat/followers following lists

See merge request !46
parents 46a16d1a 0bfe8997
Pipeline #30352 passed with stages
in 5 minutes and 22 seconds
......@@ -23,6 +23,34 @@ const Users = {
queries
})
},
async followers ({ config, fullUrl, params, queries }) {
if (fullUrl) {
return utils.request({
config,
params,
fullUrl
})
}
return utils.request({
config,
url: `${ACCOUNTS_URL}/${params.id}/followers`,
queries
})
},
async following ({ config, fullUrl, params, queries }) {
if (fullUrl) {
return utils.request({
config,
params,
fullUrl
})
}
return utils.request({
config,
url: `${ACCOUNTS_URL}/${params.id}/following`,
queries
})
},
async register ({ config, params }) {
return utils.request({
config,
......
......@@ -14,6 +14,8 @@ const initialState = {
notifications: {},
conversations: {},
userStatuses: {},
userFollowers: {},
userFollowing: {},
conversation: {},
tagTimeline: {},
polls: {},
......
......@@ -2,8 +2,8 @@ import reduce from 'lodash/reduce'
import map from 'lodash/map'
import slice from 'lodash/slice'
import forEach from 'lodash/forEach'
import { addStatusIds } from '../utils/status_utils'
import { emojifyStatus } from '../utils/parse_utils'
import { addIdsToList } from '../utils/common_utils'
const initialState = {
statusesByIds: {},
......@@ -40,7 +40,7 @@ const addStatusIdsToTimeline = (state, { statusIds, timelineName }) => {
let timeline = state.timelines[timelineName] || { ...initialTimeline }
timeline = {
...timeline,
statusIds: addStatusIds(timeline.statusIds, statusIds)
statusIds: addIdsToList(timeline.statusIds, statusIds)
}
return {
...state,
......@@ -89,7 +89,7 @@ const addTagTimeline = (state, { statuses }) => {
return {
...newState,
tag: addStatusIds(timeline, map(statuses, 'id'))
tag: addIdsToList(timeline, map(statuses, 'id'))
}
}
......
import { reduce } from 'lodash'
import { emojify, emojifyStatus } from '../utils/parse_utils'
import { addStatuses } from '../utils/status_utils'
import { addIdsToList } from '../utils/common_utils'
const initialState = {
usersByIds: {},
......@@ -63,6 +64,31 @@ const addUserStatuses = (state, { userId, statuses }) => {
}
}
const addUserList = (state, { userId, listName, items }) => {
const oldUser = state.usersByIds[userId] || {}
const user = {
...oldUser,
[listName]: addIdsToList(oldUser[listName] || [],
items ? items.map(account => account.id) : [])
}
return {
...state,
usersByIds: {
...state.usersByIds,
[userId]: user
}
}
}
const addUserFollowers = (state, { userId, followers }) => {
return addUserList(state, { userId, listName: 'followers', items: followers })
}
const addUserFollowing = (state, { userId, following }) => {
return addUserList(state, { userId, listName: 'following', items: following })
}
const deleteUserStatus = (state, { userId, statusId }) => {
const oldUser = state.usersByIds[userId] || {}
const user = {
......@@ -98,6 +124,8 @@ const reducers = {
setCurrentUser,
updateCurrentUser,
addUserStatuses,
addUserFollowers,
addUserFollowing,
deleteUserStatus,
updateUnreadNotificationsCount
}
......@@ -133,6 +161,18 @@ const actions = {
payload: { userId, statuses }
}
},
addUserFollowers: ({ userId, followers }) => {
return {
type: 'addUserFollowers',
payload: { userId, followers }
}
},
addUserFollowing: ({ userId, following }) => {
return {
type: 'addUserFollowing',
payload: { userId, following }
}
},
deleteUserStatus: ({ userId, statusId }) => {
return {
type: 'deleteUserStatus',
......
......@@ -6,7 +6,7 @@ import usersThunks from './users_thunks.js'
import has from 'lodash/has'
import pollsThunks from './polls_thunks.js'
import conversationsThunks from './conversations_thunks.js'
import { ENTITIES } from './api_thunks_entities_config'
import { ENTITIES, LOAD_OLDER_USER_FIELDS_CONFIG } from './api_thunks_entities_config'
import tagsThunks from './tags_thunks.js'
import { apiErrorCatcher, getConfig } from '../utils/api_utils'
import reducers from '../reducers'
......@@ -158,6 +158,26 @@ const generateApiThunks = () => {
}))
}
},
loadOlderUserList: ({ params, queries, entity }) => {
const loadOlderEntityConfig = LOAD_OLDER_USER_FIELDS_CONFIG[entity]
if (!entity || !loadOlderEntityConfig) return
return async (dispatch, getState) => {
const items = getState().api[loadOlderEntityConfig.reducerField] || {}
const config = getState().api.config
const fullUrl = (items.next || {}).url
return dispatch(usersThunks[loadOlderEntityConfig.thunk]({
older: true,
config,
fullUrl,
queries,
params
}))
}
},
startFetchingPoll: ({ params }) => {
return async (dispatch, getState) => {
const polls = getState().api.polls || {}
......@@ -301,4 +321,10 @@ const generateApiThunks = () => {
})
return apiThunks
}
export default generateApiThunks()
export default {
...generateApiThunks(),
updateLinks,
clearLinks,
startLoading,
stopLoading
}
......@@ -26,3 +26,14 @@ export const ENTITIES = {
clearThunk: 'clear'
}
}
export const LOAD_OLDER_USER_FIELDS_CONFIG = {
followers: {
reducerField: 'userFollowers',
thunk: 'fetchUserFollowers'
},
following: {
reducerField: 'userFollowing',
thunk: 'fetchUserFollowing'
}
}
import usersApi from '../api/users'
import Users from '../reducers/users'
import Statuses from '../reducers/statuses'
import ApiReducer from '../reducers/api'
import { apiErrorCatcher, getConfig } from '../utils/api_utils'
import { updateLinks, startLoading, stopLoading } from './api_thunks'
import { getUsersFromStatusesList } from '../utils/users_utils'
......@@ -27,7 +28,7 @@ const usersThunks = {
if (older && !fullUrl && (!queries || !queries['max_id'])) {
const userStatuses = (getState().users.usersByIds[params.id] || {}).statuses || []
queries = queries || {}
queries['max_id'] = userStatuses[userStatuses.length - 1].id
queries['max_id'] = userStatuses.length ? userStatuses[userStatuses.length - 1].id : null
}
startLoading({ dispatch, entity: 'userStatuses', older })
const result = await usersApi.statuses({ config: getConfig(getState, config), fullUrl, params, queries })
......@@ -45,6 +46,60 @@ const usersThunks = {
}
},
fetchUserFollowers: ({ config, fullUrl, params, queries, older }) => {
return async (dispatch, getState) => {
if (older && !fullUrl && (!queries || !queries['max_id'])) {
const userFollowers = (getState().users.usersByIds[params.id] || {}).followers || []
queries = queries || {}
queries['max_id'] = userFollowers.length ? userFollowers[userFollowers.length - 1].id : null
}
startLoading({ dispatch, entity: 'userFollowers', older })
const result = await usersApi.followers({ config: getConfig(getState, config), fullUrl, params, queries })
.then(res => apiErrorCatcher(res))
stopLoading({ dispatch, entity: 'userFollowers', older })
await Promise.all([
dispatch(Users.actions.addUsers({ users: result.data })),
dispatch(Users.actions.addUserFollowers({ userId: params.id, followers: result.data }))
])
if (result.links) {
await updateLinks({ dispatch, entity: 'userFollowers', links: result.links, older })
if (result.links.next) {
await dispatch(ApiReducer.actions.setNext({ entity: 'userFollowers', next: result.links.next }))
}
}
return getState()
}
},
fetchUserFollowing: ({ config, fullUrl, params, queries, older }) => {
return async (dispatch, getState) => {
if (older && !fullUrl && (!queries || !queries['max_id'])) {
const userFollowing = (getState().users.usersByIds[params.id] || {}).following || []
queries = queries || {}
queries['max_id'] = userFollowing.length ? userFollowing[userFollowing.length - 1].id : null
}
startLoading({ dispatch, entity: 'userFollowing', older })
const result = await usersApi.following({ config: getConfig(getState, config), fullUrl, params, queries })
.then(res => apiErrorCatcher(res))
stopLoading({ dispatch, entity: 'userStatuses', older })
await Promise.all([
dispatch(Users.actions.addUsers({ users: result.data })),
dispatch(Users.actions.addUserFollowing({ userId: params.id, following: result.data }))
])
if (result.links) {
await updateLinks({ dispatch, entity: 'userFollowing', links: result.links, older })
if (result.links.next) {
await dispatch(ApiReducer.actions.setNext({ entity: 'userFollowing', next: result.links.next }))
}
}
return getState()
}
},
toggleFollowState: ({ config, params }) => {
return async (dispatch, getState) => {
const result = await usersApi.toggleFollow({ config: getConfig(getState, config), params })
......
import sortedUniq from 'lodash/sortedUniq'
export const descId = (idA, idB) => idA > idB ? -1 : 1
export const addIdsToList = (oldIds, newIds) => {
const ids = oldIds.concat(newIds).sort(descId)
return sortedUniq(ids)
}
import sortedUniq from 'lodash/sortedUniq'
import sortBy from 'lodash/sortBy'
import uniqBy from 'lodash/uniqBy'
export const descId = (idA, idB) => idA > idB ? -1 : 1
export const addStatusIds = (oldIds, newIds) => {
const statusIds = oldIds.concat(newIds).sort(descId)
return sortedUniq(statusIds)
}
export const addStatuses = (oldStatuses, newStatuses) => {
const statuses = sortBy(oldStatuses.concat(newStatuses), ({ id }) => id).reverse()
......
......@@ -93,6 +93,47 @@ describe('User api', () => {
})
})
describe('/api/v1/accounts/:id/followers', () => {
it('returns the followers and the links', async () => {
const id = 1
fetch.mockImplementationOnce(fetchMocker(
[{ id: 1 }, { id: 2 }],
{
expectedUrl: `https://pleroma.soykaf.com/api/v1/accounts/${id}/followers?max_id=1`,
headers: {
link: '<https://pleroma.soykaf.com/api/v1/accounts/1/statuses?max_id=9gZ5VYhDG8GeCL8Vay>; rel="next", <https://pleroma.soykaf.com/api/v1/accounts/1/statuses?since_id=9gZ5g5Q6RlaAaN9Z5M>; rel="prev"'
}
}))
const res = await api.users.followers({ config, params: { id }, queries: { max_id: 1 } })
expect(res.state).toBe('ok')
expect(res.data).toEqual([{ id: 1 }, { id: 2 }])
expect(res.links).not.toBe(null)
})
})
describe('/api/v1/accounts/:id/following', () => {
it('returns the following and the links', async () => {
const id = 1
fetch.mockImplementationOnce(fetchMocker(
[{ id: 1 }, { id: 2 }],
{
expectedUrl: `https://pleroma.soykaf.com/api/v1/accounts/${id}/following?max_id=1`,
headers: {
link: '<https://pleroma.soykaf.com/api/v1/accounts/1/following?max_id=9gZ5VYhDG8GeCL8Vay>; rel="next", <https://pleroma.soykaf.com/api/v1/accounts/1/statuses?since_id=9gZ5g5Q6RlaAaN9Z5M>; rel="prev"'
}
}))
const res = await api.users.following({ config, params: { id }, queries: { max_id: 1 } })
expect(res.state).toBe('ok')
expect(res.data).toEqual([{ id: 1 }, { id: 2 }])
expect(res.links).not.toBe(null)
})
})
describe('POST /api/v1/accounts', () => {
it('returns a new account', async () => {
const id = 1
......
......@@ -133,15 +133,63 @@ describe('User reducers', () => {
expect(resultState.usersByIds['1']).toEqual({ ...user, statuses: [{ id: 2, content: 'b' }] })
})
it('update unread notifications count', () => {
const currentUser = { id: '1', pleroma: { unread_notifications_count: 5 } }
})
describe(`adding a user's followers`, () => {
it('add followersIds no a new user', () => {
const followers = [{ id: 2, acct: 'b' }, { id: 1, acct: 'a' }]
const resultState = Users.reducer(
undefined,
Users.actions.addUserFollowers({ userId: '1', followers })
)
expect(resultState.usersByIds['1']).toEqual({ followers: [2, 1] })
})
it('update user followersIds', () => {
const user = { id: 1 }
const followers = [{ id: 2, acct: 'b' }, { id: 1, acct: 'a' }]
const resultState = Users.reducer(
{ usersByIds: { 1: user } },
Users.actions.addUserFollowers({ userId: user.id, followers })
)
expect(resultState.usersByIds['1']).toEqual({ ...user, followers: [2, 1] })
})
})
describe(`adding a user's following`, () => {
it('add followingIds no a new user', () => {
const following = [{ id: 2, acct: 'b' }, { id: 1, acct: 'a' }]
const resultState = Users.reducer(
{ currentUser },
Users.actions.updateUnreadNotificationsCount({ unreadNotificationsCount: 1 })
undefined,
Users.actions.addUserFollowing({ userId: '1', following })
)
expect(resultState.currentUser).toEqual({ id: '1', pleroma: { unread_notifications_count: 1 } })
expect(resultState.usersByIds['1']).toEqual({ following: [2, 1] })
})
it('update user followingIds', () => {
const user = { id: 1 }
const following = [{ id: 2, acct: 'b' }, { id: 1, acct: 'a' }]
const resultState = Users.reducer(
{ usersByIds: { 1: user } },
Users.actions.addUserFollowing({ userId: user.id, following })
)
expect(resultState.usersByIds['1']).toEqual({ ...user, following: [2, 1] })
})
})
it('update unread notifications count', () => {
const currentUser = { id: '1', pleroma: { unread_notifications_count: 5 } }
const resultState = Users.reducer(
{ currentUser },
Users.actions.updateUnreadNotificationsCount({ unreadNotificationsCount: 1 })
)
expect(resultState.currentUser).toEqual({ id: '1', pleroma: { unread_notifications_count: 1 } })
})
})
......@@ -104,6 +104,64 @@ describe('Users thunks', () => {
.toEqual(expectedResult)
})
it(`fetch user's followers`, async () => {
const store = { state: undefined }
const dispatch = (action) => {
store.state = reducer(store.state, action)
}
const getState = () => store.state
const followers = [
{ id: 1, acct: 'user1' },
{ id: 0, acct: 'user2' },
]
fetch.mockReset()
fetch
.mockImplementationOnce(fetchMocker(
followers,
{ expectedUrl: `https://pleroma.soykaf.com/api/v1/accounts/2/followers` }
))
let state = await usersThunks.fetchUserFollowers({ config, params: { id: '2' } })(dispatch, getState)
const expectedResult = {
'0': { id: 0, acct: 'user2', display_name: undefined, note: undefined },
'1': { id: 1, acct: 'user1', display_name: undefined, note: undefined },
'2': { followers: [1, 0], display_name: undefined, note: undefined }
}
expect(state.users.usersByIds)
.toEqual(expectedResult)
})
it(`fetch user's following`, async () => {
const store = { state: undefined }
const dispatch = (action) => {
store.state = reducer(store.state, action)
}
const getState = () => store.state
const following = [
{ id: 1, acct: 'user1' },
{ id: 0, acct: 'user2' },
]
fetch.mockReset()
fetch
.mockImplementationOnce(fetchMocker(
following,
{ expectedUrl: `https://pleroma.soykaf.com/api/v1/accounts/2/following` }
))
let state = await usersThunks.fetchUserFollowing({ config, params: { id: '2' } })(dispatch, getState)
const expectedResult = {
'0': { id: 0, acct: 'user2', display_name: undefined, note: undefined },
'1': { id: 1, acct: 'user1', display_name: undefined, note: undefined },
'2': { following: [1, 0], display_name: undefined, note: undefined }
}
expect(state.users.usersByIds)
.toEqual(expectedResult)
})
it('follow user', async () => {
const store = { state: {
users: {
......
import * as StatusUtils from '../../src/utils/status_utils'
import * as CommonUtils from '../../src/utils/common_utils'
describe('status utils', () => {
describe('addStatusIds', () => {
describe('common utils', () => {
describe('addIdsToList', () => {
it('should add ids and reverse sort them', () => {
const oldIds = ['b', 'c', 'd']
const newIds = ['a', 'e']
const expected = ['e', 'd', 'c', 'b', 'a']
expect(StatusUtils.addStatusIds(oldIds, newIds)).toEqual(expected)
expect(CommonUtils.addIdsToList(oldIds, newIds)).toEqual(expected)
})
it('should only keep unique ids', () => {
const oldIds = ['b', 'c']
const newIds = ['c', 'e']
const expected = ['e', 'c', 'b']
expect(StatusUtils.addStatusIds(oldIds, newIds)).toEqual(expected)
expect(CommonUtils.addIdsToList(oldIds, newIds)).toEqual(expected)
})
})
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment