From aa01be831f3e2568c5a6069bcb39ca3fe314aa8a Mon Sep 17 00:00:00 2001 From: enahum Date: Tue, 13 Dec 2016 11:07:16 -0300 Subject: [PATCH] include posts into store (#125) * include posts into store * addressing feedback --- service/actions/posts.js | 181 ++++++++++++- service/actions/preferences.js | 18 +- service/client/client.js | 61 ++++- service/constants/constants.js | 4 +- service/constants/posts.js | 36 ++- service/constants/preferences.js | 1 - service/reducers/entities/helpers.js | 130 --------- service/reducers/entities/posts.js | 196 ++++++++++++-- service/reducers/entities/users.js | 34 ++- service/reducers/requests/index.js | 10 +- service/reducers/requests/posts.js | 98 +++++++ test/service/actions/posts.test.js | 384 +++++++++++++++++++++++++++ test/service/actions/users.test.js | 1 - 13 files changed, 966 insertions(+), 188 deletions(-) delete mode 100644 service/reducers/entities/helpers.js create mode 100644 service/reducers/requests/posts.js create mode 100644 test/service/actions/posts.test.js diff --git a/service/actions/posts.js b/service/actions/posts.js index 3e5b79285..1f0999413 100644 --- a/service/actions/posts.js +++ b/service/actions/posts.js @@ -1,17 +1,182 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {bindClientFunc} from './helpers.js'; import Client from 'service/client'; -import {PostsTypes} from 'service/constants'; +import {batchActions} from 'redux-batched-actions'; +import {bindClientFunc, forceLogoutIfNecessary} from './helpers'; +import {Constants, PostsTypes} from 'service/constants'; -export function fetchPosts(teamId, channelId) { +export function createPost(teamId, post) { return bindClientFunc( - Client.fetchPosts, - PostsTypes.FETCH_POSTS_REQUEST, - PostsTypes.FETCH_POSTS_SUCCESS, - PostsTypes.FETCH_POSTS_FAILURE, + Client.createPost, + PostsTypes.CREATE_POST_REQUEST, + [PostsTypes.RECEIVED_POST, PostsTypes.CREATE_POST_SUCCESS], + PostsTypes.CREATE_POST_FAILURE, teamId, - channelId + post ); } + +export function editPost(teamId, post) { + return bindClientFunc( + Client.editPost, + PostsTypes.EDIT_POST_REQUEST, + [PostsTypes.RECEIVED_POST, PostsTypes.EDIT_POST_SUCCESS], + PostsTypes.EDIT_POST_FAILURE, + teamId, + post + ); +} + +export function deletePost(teamId, post) { + return async (dispatch, getState) => { + try { + dispatch({type: PostsTypes.DELETE_POST_REQUEST}, getState); + + await Client.deletePost(teamId, post.channel_id, post.id); + dispatch(batchActions([ + { + type: PostsTypes.POST_DELETED, + data: post + }, + { + type: PostsTypes.DELETE_POST_SUCCESS + } + ]), getState); + } catch (error) { + forceLogoutIfNecessary(error, dispatch); + dispatch({type: PostsTypes.DELETE_POST_FAILURE, error}, getState); + } + }; +} + +export function removePost(post) { + return async (dispatch, getState) => { + dispatch({ + type: PostsTypes.REMOVE_POST, + data: post + }, getState); + }; +} + +export function getPost(teamId, channelId, postId) { + return async (dispatch, getState) => { + try { + dispatch({type: PostsTypes.GET_POST_REQUEST}, getState); + const post = await Client.getPost(teamId, channelId, postId); + dispatch(batchActions([ + { + type: PostsTypes.RECEIVED_POSTS, + data: post, + channelId + }, + { + type: PostsTypes.GET_POST_SUCCESS + } + ]), getState); + } catch (error) { + forceLogoutIfNecessary(error, dispatch); + dispatch({type: PostsTypes.GET_POST_FAILURE, error}, getState); + } + }; +} + +export function getPosts(teamId, channelId, offset = 0, limit = Constants.POST_CHUNK_SIZE) { + return async (dispatch, getState) => { + try { + dispatch({type: PostsTypes.GET_POSTS_REQUEST}, getState); + const posts = await Client.getPosts(teamId, channelId, offset, limit); + dispatch(batchActions([ + { + type: PostsTypes.RECEIVED_POSTS, + data: posts, + channelId + }, + { + type: PostsTypes.GET_POSTS_SUCCESS + } + ]), getState); + } catch (error) { + forceLogoutIfNecessary(error, dispatch); + dispatch({type: PostsTypes.GET_POSTS_FAILURE, error}, getState); + } + }; +} + +export function getPostsSince(teamId, channelId, since) { + return async (dispatch, getState) => { + try { + dispatch({type: PostsTypes.GET_POSTS_SINCE_REQUEST}, getState); + const posts = await Client.getPostsSince(teamId, channelId, since); + dispatch(batchActions([ + { + type: PostsTypes.RECEIVED_POSTS, + data: posts, + channelId + }, + { + type: PostsTypes.GET_POSTS_SINCE_SUCCESS + } + ]), getState); + } catch (error) { + forceLogoutIfNecessary(error, dispatch); + dispatch({type: PostsTypes.GET_POSTS_SINCE_FAILURE, error}, getState); + } + }; +} + +export function getPostsBefore(teamId, channelId, postId, offset = 0, limit = Constants.POST_CHUNK_SIZE) { + return async (dispatch, getState) => { + try { + dispatch({type: PostsTypes.GET_POSTS_BEFORE_REQUEST}, getState); + const posts = await Client.getPostsBefore(teamId, channelId, postId, offset, limit); + dispatch(batchActions([ + { + type: PostsTypes.RECEIVED_POSTS, + data: posts, + channelId + }, + { + type: PostsTypes.GET_POSTS_BEFORE_SUCCESS + } + ]), getState); + } catch (error) { + forceLogoutIfNecessary(error, dispatch); + dispatch({type: PostsTypes.GET_POSTS_BEFORE_FAILURE, error}, getState); + } + }; +} + +export function getPostsAfter(teamId, channelId, postId, offset = 0, limit = Constants.POST_CHUNK_SIZE) { + return async (dispatch, getState) => { + try { + dispatch({type: PostsTypes.GET_POSTS_AFTER_REQUEST}, getState); + const posts = await Client.getPostsAfter(teamId, channelId, postId, offset, limit); + dispatch(batchActions([ + { + type: PostsTypes.RECEIVED_POSTS, + data: posts, + channelId + }, + { + type: PostsTypes.GET_POSTS_AFTER_SUCCESS + } + ]), getState); + } catch (error) { + forceLogoutIfNecessary(error, dispatch); + dispatch({type: PostsTypes.GET_POSTS_AFTER_FAILURE, error}, getState); + } + }; +} + +export default { + createPost, + editPost, + deletePost, + removePost, + getPost, + getPosts, + getPostsSince, + getPostsBefore, + getPostsAfter +}; diff --git a/service/actions/preferences.js b/service/actions/preferences.js index c28ad681b..7462cccb1 100644 --- a/service/actions/preferences.js +++ b/service/actions/preferences.js @@ -4,7 +4,7 @@ import Client from 'service/client'; import {PreferencesTypes} from 'service/constants'; -import {bindClientFunc, forceLogoutIfNecessary, requestData, requestFailure} from './helpers'; +import {bindClientFunc, forceLogoutIfNecessary} from './helpers'; import {batchActions} from 'redux-batched-actions'; @@ -19,7 +19,7 @@ export function getMyPreferences() { export function savePreferences(preferences) { return async (dispatch, getState) => { - dispatch(requestData(PreferencesTypes.SAVE_PREFERENCES_REQUEST), getState); + dispatch({type: PreferencesTypes.SAVE_PREFERENCES_REQUEST}, getState); try { await Client.savePreferences(preferences); @@ -33,16 +33,16 @@ export function savePreferences(preferences) { type: PreferencesTypes.SAVE_PREFERENCES_SUCCESS } ]), getState); - } catch (err) { - forceLogoutIfNecessary(err, dispatch); - dispatch(requestFailure(PreferencesTypes.SAVE_PREFERENCES_FAILURE, err), getState); + } catch (error) { + forceLogoutIfNecessary(error, dispatch); + dispatch({type: PreferencesTypes.SAVE_PREFERENCES_FAILURE, error}, getState); } }; } export function deletePreferences(preferences) { return async (dispatch, getState) => { - dispatch(requestData(PreferencesTypes.DELETE_PREFERENCES_REQUEST), getState); + dispatch({type: PreferencesTypes.DELETE_PREFERENCES_REQUEST}, getState); try { await Client.deletePreferences(preferences); @@ -56,9 +56,9 @@ export function deletePreferences(preferences) { type: PreferencesTypes.DELETE_PREFERENCES_SUCCESS } ]), getState); - } catch (err) { - forceLogoutIfNecessary(err, dispatch); - dispatch(requestFailure(PreferencesTypes.DELETE_PREFERENCES_FAILURE, err), getState); + } catch (error) { + forceLogoutIfNecessary(error, dispatch); + dispatch({type: PreferencesTypes.DELETE_PREFERENCES_FAILURE, error}, getState); } }; } diff --git a/service/client/client.js b/service/client/client.js index 17e101cdb..888abbed8 100644 --- a/service/client/client.js +++ b/service/client/client.js @@ -499,17 +499,6 @@ export default class Client { }; // Post routes - - fetchPosts = (teamId, channelId, onRequest, onSuccess, onFailure) => { - return this.doFetch( - `${this.getPostsRoute(teamId, channelId)}/page/0/60`, - {method: 'get'}, - onRequest, - onSuccess, - onFailure - ); - }; - createPost = async (teamId, post) => { return this.doFetch( `${this.getPostsRoute(teamId, post.channel_id)}/create`, @@ -517,6 +506,56 @@ export default class Client { ); }; + editPost = async (teamId, post) => { + return this.doFetch( + `${this.getPostsRoute(teamId, post.channel_id)}/update`, + {method: 'post', body: JSON.stringify(post)} + ); + }; + + deletePost = async (teamId, channelId, postId) => { + return this.doFetch( + `${this.getPostsRoute(teamId, channelId)}/${postId}/delete`, + {method: 'post'} + ); + }; + + getPost = async (teamId, channelId, postId) => { + return this.doFetch( + `${this.getPostsRoute(teamId, channelId)}/${postId}/get`, + {method: 'get'} + ); + }; + + getPosts = async (teamId, channelId, offset, limit) => { + return this.doFetch( + `${this.getPostsRoute(teamId, channelId)}/page/${offset}/${limit}`, + {method: 'get'} + ); + }; + + getPostsSince = async (teamId, channelId, since) => { + return this.doFetch( + `${this.getPostsRoute(teamId, channelId)}/since/${since}`, + {method: 'get'} + ); + }; + + getPostsBefore = async (teamId, channelId, postId, offset, limit) => { + return this.doFetch( + `${this.getPostsRoute(teamId, channelId)}/${postId}/before/${offset}/${limit}`, + {method: 'get'} + ); + }; + + getPostsAfter = async (teamId, channelId, postId, offset, limit) => { + return this.doFetch( + `${this.getPostsRoute(teamId, channelId)}/${postId}/after/${offset}/${limit}`, + {method: 'get'} + ); + }; + + // Client helpers // Preferences routes getMyPreferences = async () => { diff --git a/service/constants/constants.js b/service/constants/constants.js index f0cd7ebdf..8c5ef28fd 100644 --- a/service/constants/constants.js +++ b/service/constants/constants.js @@ -10,7 +10,9 @@ const Constants = { TEAM_ADMIN_ROLE: 'team_admin', CHANNEL_USER_ROLE: 'channel_user', - CHANNEL_ADMIN_ROLE: 'channel_admin' + CHANNEL_ADMIN_ROLE: 'channel_admin', + + POST_DELETED: 'DELETED' }; export default Constants; diff --git a/service/constants/posts.js b/service/constants/posts.js index 4c06a5cc9..ea3851aaa 100644 --- a/service/constants/posts.js +++ b/service/constants/posts.js @@ -4,17 +4,43 @@ import keymirror from 'keymirror'; const PostsTypes = keymirror({ - FETCH_POSTS_REQUEST: null, - FETCH_POSTS_SUCCESS: null, - FETCH_POSTS_FAILURE: null, + CREATE_POST_REQUEST: null, + CREATE_POST_SUCCESS: null, + CREATE_POST_FAILURE: null, + + EDIT_POST_REQUEST: null, + EDIT_POST_SUCCESS: null, + EDIT_POST_FAILURE: null, + + DELETE_POST_REQUEST: null, + DELETE_POST_SUCCESS: null, + DELETE_POST_FAILURE: null, + + GET_POST_REQUEST: null, + GET_POST_SUCCESS: null, + GET_POST_FAILURE: null, + + GET_POSTS_REQUEST: null, + GET_POSTS_SUCCESS: null, + GET_POSTS_FAILURE: null, + + GET_POSTS_SINCE_REQUEST: null, + GET_POSTS_SINCE_SUCCESS: null, + GET_POSTS_SINCE_FAILURE: null, + + GET_POSTS_BEFORE_REQUEST: null, + GET_POSTS_BEFORE_SUCCESS: null, + GET_POSTS_BEFORE_FAILURE: null, + + GET_POSTS_AFTER_REQUEST: null, + GET_POSTS_AFTER_SUCCESS: null, + GET_POSTS_AFTER_FAILURE: null, RECEIVED_POST: null, RECEIVED_POSTS: null, RECEIVED_FOCUSED_POST: null, RECEIVED_POST_SELECTED: null, RECEIVED_EDIT_POST: null, - CREATE_POST: null, - CREATE_COMMENT: null, POST_DELETED: null, REMOVE_POST: null }); diff --git a/service/constants/preferences.js b/service/constants/preferences.js index 264773b88..d5a95e12b 100644 --- a/service/constants/preferences.js +++ b/service/constants/preferences.js @@ -16,7 +16,6 @@ export default keymirror({ DELETE_PREFERENCES_SUCCESS: null, DELETE_PREFERENCES_FAILURE: null, - RECEIVED_PREFERENCE: null, RECEIVED_PREFERENCES: null, DELETED_PREFERENCES: null }); diff --git a/service/reducers/entities/helpers.js b/service/reducers/entities/helpers.js deleted file mode 100644 index 994b36263..000000000 --- a/service/reducers/entities/helpers.js +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -function isPostListNull(pl) { - if (!pl) { - return true; - } - - if (!pl.posts) { - return true; - } - - return !pl.order; -} - -function makePostListNonNull(pl) { - let postList = {...pl}; - if (!postList) { - postList = {order: [], posts: {}}; - } - - if (!postList.order) { - postList.order = []; - } - - if (!postList.posts) { - postList.posts = {}; - } - - return postList; -} - -export function addPosts(state, action) { - const newState = {...state}; - const newPosts = action.data; - const id = action.channel_id; - - if (isPostListNull(newPosts)) { - return state; - } - - if (action.checkLatest) { - const currentLatest = newState.latestPageTime[id] || 0; - if (newPosts.order.length >= 1) { - const newLatest = newPosts.posts[newPosts.order[0]].create_at || 0; - if (newLatest > currentLatest) { - newState.latestPageTime = { - ...newState.latestPageTime, - [id]: newLatest - }; - } - } else if (currentLatest === 0) { - // Mark that an empty page was received - newState.latestPageTime = { - ...newState.latestPageTime, - [id]: 1 - }; - } - } - - const combinedPosts = {...makePostListNonNull(newState.postsInfo[id].postList)}; - - for (const pid in newPosts.posts) { - if (newPosts.posts.hasOwnProperty(pid)) { - const np = newPosts.posts[pid]; - if (np.delete_at === 0) { - combinedPosts.posts[pid] = np; - if (combinedPosts.order.indexOf(pid) === -1 && newPosts.order.indexOf(pid) !== -1) { - combinedPosts.order.push(pid); - } - } else if (combinedPosts.posts.hasOwnProperty(pid)) { - combinedPosts.posts[pid] = {...np, state: 'DELETED', fileIds: []}; - } - } - } - - combinedPosts.order.sort((a, b) => { - if (combinedPosts.posts[a].create_at > combinedPosts.posts[b].create_at) { - return -1; - } - if (combinedPosts.posts[a].create_at < combinedPosts.posts[b].create_at) { - return 1; - } - - return 0; - }); - - newState.postsInfo = { - ...newState.postsInfo, - [id]: { - ...newState.postsInfo[id], - postList: combinedPosts - } - }; - - return newState; -} - -export function profilesToSet(state, action) { - const id = action.id; - const nextSet = new Set(state[id]); - Object.keys(action.data).forEach((key) => { - nextSet.add(key); - }); - - return { - ...state, - [id]: nextSet - }; -} - -export function addProfileToSet(state, action) { - const id = action.id; - const nextSet = new Set(state[id]); - nextSet.add(action.data.user_id); - return { - ...state, - [id]: nextSet - }; -} - -export function removeProfileFromSet(state, action) { - const id = action.id; - const nextSet = new Set(state[id]); - nextSet.delete(action.data.user_id); - return { - ...state, - [id]: nextSet - }; -} diff --git a/service/reducers/entities/posts.js b/service/reducers/entities/posts.js index d063e8d81..a95075f3a 100644 --- a/service/reducers/entities/posts.js +++ b/service/reducers/entities/posts.js @@ -1,9 +1,177 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {PostsTypes, UsersTypes} from 'service/constants'; +import {Constants, PostsTypes, UsersTypes} from 'service/constants'; import {combineReducers} from 'redux'; -import {addPosts} from './helpers'; + +function isPostListNull(pl) { + if (!pl) { + return true; + } + + if (!pl.posts) { + return true; + } + + return !pl.order; +} + +function makePostListNonNull(pl) { + let postList = {...pl}; + if (!postList) { + postList = {order: [], posts: {}}; + } + + if (!postList.order) { + postList.order = []; + } + + if (!postList.posts) { + postList.posts = {}; + } + + return postList; +} + +function storePosts(state, action) { + const newPosts = action.data; + const channelId = action.channelId; + + if (isPostListNull(newPosts)) { + return state; + } + + const postList = state[channelId] || {}; + const combinedPosts = makePostListNonNull(postList); + + for (const pid in newPosts.posts) { + if (newPosts.posts.hasOwnProperty(pid)) { + const np = newPosts.posts[pid]; + if (np.delete_at === 0) { + combinedPosts.posts[pid] = np; + if (combinedPosts.order.indexOf(pid) === -1 && newPosts.order.indexOf(pid) !== -1) { + combinedPosts.order.push(pid); + } + } else if (combinedPosts.posts.hasOwnProperty(pid)) { + combinedPosts.posts[pid] = {...np, state: Constants.POST_DELETED, fileIds: []}; + } + } + } + + combinedPosts.order.sort((a, b) => { + if (combinedPosts.posts[a].create_at > combinedPosts.posts[b].create_at) { + return -1; + } + if (combinedPosts.posts[a].create_at < combinedPosts.posts[b].create_at) { + return 1; + } + + return 0; + }); + + return { + ...state, + [channelId]: combinedPosts + }; +} + +function storePost(state, action) { + const post = action.data; + const channelId = post.channel_id; + + const postList = state[channelId] || {}; + const combinedPosts = makePostListNonNull(postList); + + combinedPosts.posts[post.id] = post; + + if (combinedPosts.order.indexOf(post.id) === -1) { + combinedPosts.order.unshift(post.id); + } + + return { + ...state, + [channelId]: combinedPosts + }; +} + +function deletePost(state, action) { + const post = action.data; + const channelId = post.channel_id; + const postInfo = state[channelId]; + + if (!postInfo) { + // the post that has been deleted is in a channel that we haven't seen so just ignore it + return state; + } + + if (isPostListNull(postInfo)) { + return state; + } + + if (postInfo.posts[post.id]) { + const combinedPosts = makePostListNonNull(postInfo); + + combinedPosts.posts[post.id] = { + ...post, + state: Constants.POST_DELETED, + file_ids: [], + has_reactions: false + }; + + return { + ...state, + [channelId]: combinedPosts + }; + } + + return state; +} + +function removePost(state, action) { + const post = action.data; + const channelId = post.channel_id; + const postInfo = state[channelId]; + + if (!postInfo) { + // the post that has been deleted is in a channel that we haven't seen so just ignore it + return state; + } + + if (isPostListNull(postInfo)) { + return state; + } + + if (postInfo.posts[post.id]) { + const combinedPosts = makePostListNonNull(postInfo); + Reflect.deleteProperty(combinedPosts.posts, post.id); + + const index = combinedPosts.order.indexOf(post.id); + if (index !== -1) { + combinedPosts.order.splice(index, 1); + } + + for (const pid in combinedPosts.posts) { + if (!combinedPosts.posts.hasOwnProperty(pid)) { + continue; + } + + if (combinedPosts.posts[pid].root_id === post.id) { + Reflect.deleteProperty(combinedPosts.posts, pid); + const commentIndex = combinedPosts.order.indexOf(pid); + if (commentIndex !== -1) { + combinedPosts.order.splice(commentIndex, 1); + } + } + } + + return { + ...state, + [channelId]: combinedPosts + }; + } + + return state; +} function selectedPostId(state = '', action) { switch (action.type) { @@ -25,17 +193,14 @@ function currentFocusedPostId(state = '', action) { function postsInfo(state = {}, action) { switch (action.type) { - case PostsTypes.FETCH_POSTS_SUCCESS: - return addPosts(state, action); - case UsersTypes.LOGOUT_SUCCESS: - return {}; - default: - return state; - } -} - -function latestPageTime(state = {}, action) { - switch (action.type) { + case PostsTypes.RECEIVED_POST: + return storePost(state, action); + case PostsTypes.RECEIVED_POSTS: + return storePosts(state, action); + case PostsTypes.POST_DELETED: + return deletePost(state, action); + case PostsTypes.REMOVE_POST: + return removePost(state, action); case UsersTypes.LOGOUT_SUCCESS: return {}; default: @@ -52,8 +217,5 @@ export default combineReducers({ currentFocusedPostId, // object where every key is the channel id and has and object with the postList - postsInfo, - - // object where the every key is the channel id and has the timestamp of the latest post created in that channel - latestPageTime + postsInfo }); diff --git a/service/reducers/entities/users.js b/service/reducers/entities/users.js index 03faea9eb..a688e56fc 100644 --- a/service/reducers/entities/users.js +++ b/service/reducers/entities/users.js @@ -3,7 +3,39 @@ import {combineReducers} from 'redux'; import {UsersTypes} from 'service/constants'; -import {profilesToSet, addProfileToSet, removeProfileFromSet} from './helpers'; + +function profilesToSet(state, action) { + const id = action.id; + const nextSet = new Set(state[id]); + Object.keys(action.data).forEach((key) => { + nextSet.add(key); + }); + + return { + ...state, + [id]: nextSet + }; +} + +function addProfileToSet(state, action) { + const id = action.id; + const nextSet = new Set(state[id]); + nextSet.add(action.data.user_id); + return { + ...state, + [id]: nextSet + }; +} + +function removeProfileFromSet(state, action) { + const id = action.id; + const nextSet = new Set(state[id]); + nextSet.delete(action.data.user_id); + return { + ...state, + [id]: nextSet + }; +} function currentId(state = '', action) { switch (action.type) { diff --git a/service/reducers/requests/index.js b/service/reducers/requests/index.js index ba9db05ab..08bcf4103 100644 --- a/service/reducers/requests/index.js +++ b/service/reducers/requests/index.js @@ -3,16 +3,18 @@ import {combineReducers} from 'redux'; -import general from './general'; import channels from './channels'; -import users from './users'; +import general from './general'; +import posts from './posts'; import teams from './teams'; +import users from './users'; import preferences from './preferences'; export default combineReducers({ - general, channels, - users, + general, + posts, teams, + users, preferences }); diff --git a/service/reducers/requests/posts.js b/service/reducers/requests/posts.js new file mode 100644 index 000000000..62cc9bd4e --- /dev/null +++ b/service/reducers/requests/posts.js @@ -0,0 +1,98 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {handleRequest, initialRequestState} from './helpers'; +import {PostsTypes} from 'service/constants'; + +import {combineReducers} from 'redux'; + +function createPost(state = initialRequestState(), action) { + return handleRequest( + PostsTypes.CREATE_POST_REQUEST, + PostsTypes.CREATE_POST_SUCCESS, + PostsTypes.CREATE_POST_FAILURE, + state, + action + ); +} + +function editPost(state = initialRequestState(), action) { + return handleRequest( + PostsTypes.EDIT_POST_REQUEST, + PostsTypes.EDIT_POST_SUCCESS, + PostsTypes.EDIT_POST_FAILURE, + state, + action + ); +} + +function deletePost(state = initialRequestState(), action) { + return handleRequest( + PostsTypes.DELETE_POST_REQUEST, + PostsTypes.DELETE_POST_SUCCESS, + PostsTypes.DELETE_POST_FAILURE, + state, + action + ); +} + +function getPost(state = initialRequestState(), action) { + return handleRequest( + PostsTypes.GET_POST_REQUEST, + PostsTypes.GET_POST_SUCCESS, + PostsTypes.GET_POST_FAILURE, + state, + action + ); +} + +function getPosts(state = initialRequestState(), action) { + return handleRequest( + PostsTypes.GET_POSTS_REQUEST, + PostsTypes.GET_POSTS_SUCCESS, + PostsTypes.GET_POSTS_FAILURE, + state, + action + ); +} + +function getPostsSince(state = initialRequestState(), action) { + return handleRequest( + PostsTypes.GET_POSTS_SINCE_REQUEST, + PostsTypes.GET_POSTS_SINCE_SUCCESS, + PostsTypes.GET_POSTS_SINCE_FAILURE, + state, + action + ); +} + +function getPostsBefore(state = initialRequestState(), action) { + return handleRequest( + PostsTypes.GET_POSTS_BEFORE_REQUEST, + PostsTypes.GET_POSTS_BEFORE_SUCCESS, + PostsTypes.GET_POSTS_BEFORE_FAILURE, + state, + action + ); +} + +function getPostsAfter(state = initialRequestState(), action) { + return handleRequest( + PostsTypes.GET_POSTS_AFTER_REQUEST, + PostsTypes.GET_POSTS_AFTER_SUCCESS, + PostsTypes.GET_POSTS_AFTER_FAILURE, + state, + action + ); +} + +export default combineReducers({ + createPost, + editPost, + deletePost, + getPost, + getPosts, + getPostsSince, + getPostsBefore, + getPostsAfter +}); diff --git a/test/service/actions/posts.test.js b/test/service/actions/posts.test.js new file mode 100644 index 000000000..535cb6d58 --- /dev/null +++ b/test/service/actions/posts.test.js @@ -0,0 +1,384 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import assert from 'assert'; + +import * as Actions from 'service/actions/posts'; +import Client from 'service/client'; +import configureStore from 'app/store'; +import {Constants, RequestStatus} from 'service/constants'; +import TestHelper from 'test/test_helper'; + +describe('Actions.Posts', () => { + it('createPost', (done) => { + TestHelper.initBasic(Client).then(() => { + const store = configureStore(); + + store.subscribe(() => { + const createRequest = store.getState().requests.posts.createPost; + const postsInfo = store.getState().entities.posts.postsInfo; + + if (createRequest.status === RequestStatus.SUCCESS || createRequest.status === RequestStatus.FAILURE) { + if (createRequest.error) { + done(new Error(JSON.stringify(createRequest.error))); + } else { + assert.ok(postsInfo[TestHelper.basicChannel.id]); + assert.ok(postsInfo[TestHelper.basicChannel.id].order.length); + assert.ok(Object.keys(postsInfo[TestHelper.basicChannel.id].posts).length); + done(); + } + } + }); + + const post = TestHelper.fakePost(TestHelper.basicChannel.id); + Actions.createPost( + TestHelper.basicTeam.id, + post + )(store.dispatch, store.getState); + }); + }); + + it('editPost', (done) => { + TestHelper.initBasic(Client).then(async () => { + const store = configureStore(); + const post = await Client.createPost( + TestHelper.basicTeam.id, + TestHelper.fakePost(TestHelper.basicChannel.id) + ); + const message = post.message; + + store.subscribe(() => { + const editRequest = store.getState().requests.posts.editPost; + const postsInfo = store.getState().entities.posts.postsInfo; + + if (editRequest.status === RequestStatus.SUCCESS || editRequest.status === RequestStatus.FAILURE) { + if (editRequest.error) { + done(new Error(JSON.stringify(editRequest.error))); + } else { + assert.ok(postsInfo[TestHelper.basicChannel.id]); + assert.ok(postsInfo[TestHelper.basicChannel.id].order.length); + assert.strictEqual( + postsInfo[TestHelper.basicChannel.id].posts[post.id].message, + `${message} (edited)` + ); + done(); + } + } + }); + + post.message = `${message} (edited)`; + Actions.editPost( + TestHelper.basicTeam.id, + post + )(store.dispatch, store.getState); + }); + }); + + it('deletePost', (done) => { + TestHelper.initBasic(Client).then(async () => { + const store = configureStore(); + let created; + + store.subscribe(() => { + const createRequest = store.getState().requests.posts.createPost; + const deleteRequest = store.getState().requests.posts.deletePost; + const postsInfo = store.getState().entities.posts.postsInfo; + + if (deleteRequest.status === RequestStatus.SUCCESS || deleteRequest.status === RequestStatus.FAILURE) { + if (deleteRequest.error) { + done(new Error(JSON.stringify(deleteRequest.error))); + } else { + assert.ok(postsInfo[TestHelper.basicChannel.id]); + assert.strictEqual( + postsInfo[TestHelper.basicChannel.id].posts[created.id].state, + Constants.POST_DELETED + ); + done(); + } + } + + if (deleteRequest.status === RequestStatus.NOT_STARTED && + (createRequest.status === RequestStatus.SUCCESS || createRequest.status === RequestStatus.FAILURE)) { + if (createRequest.error) { + done(new Error(JSON.stringify(createRequest.error))); + } else { + const posts = postsInfo[TestHelper.basicChannel.id].posts; + created = posts[Object.keys(posts)[0]]; + Actions.deletePost(TestHelper.basicTeam.id, created)(store.dispatch, store.getState); + } + } + }); + + const post = TestHelper.fakePost(TestHelper.basicChannel.id); + Actions.createPost( + TestHelper.basicTeam.id, + post + )(store.dispatch, store.getState); + }); + }); + + it('removePost', (done) => { + TestHelper.initBasic(Client).then(async () => { + const store = configureStore(); + const post1a = await Client.createPost( + TestHelper.basicTeam.id, + {...TestHelper.fakePost(TestHelper.basicChannel.id), root_id: TestHelper.basicPost.id} + ); + + await Actions.getPosts( + TestHelper.basicTeam.id, + TestHelper.basicChannel.id + )(store.dispatch, store.getState); + + store.subscribe(() => { + const postsInfo = store.getState().entities.posts.postsInfo; + const postList = postsInfo[TestHelper.basicChannel.id]; + assert.strictEqual(postList.order.length, 0); + assert.ifError(postList.posts[post1a.id]); + assert.ifError(postList.posts[TestHelper.basicPost.id]); + done(); + }); + + Actions.removePost( + TestHelper.basicPost + )(store.dispatch, store.getState); + }); + }); + + it('getPost', (done) => { + TestHelper.initBasic(Client).then(async () => { + const store = configureStore(); + const post = await Client.createPost( + TestHelper.basicTeam.id, + TestHelper.fakePost(TestHelper.basicChannel.id) + ); + + store.subscribe(() => { + const getRequest = store.getState().requests.posts.getPost; + const postsInfo = store.getState().entities.posts.postsInfo; + + if (getRequest.status === RequestStatus.SUCCESS || getRequest.status === RequestStatus.FAILURE) { + if (getRequest.error) { + done(new Error(JSON.stringify(getRequest.error))); + } else { + assert.ok(postsInfo[TestHelper.basicChannel.id]); + assert.ok(postsInfo[TestHelper.basicChannel.id].order.length); + assert.ok(postsInfo[TestHelper.basicChannel.id].posts[post.id]); + done(); + } + } + }); + + Actions.getPost( + TestHelper.basicTeam.id, + TestHelper.basicChannel.id, + post.id + )(store.dispatch, store.getState); + }); + }); + + it('getPosts', (done) => { + TestHelper.initBasic(Client).then(async () => { + const store = configureStore(); + const post1 = await Client.createPost( + TestHelper.basicTeam.id, + TestHelper.fakePost(TestHelper.basicChannel.id) + ); + const post1a = await Client.createPost( + TestHelper.basicTeam.id, + {...TestHelper.fakePost(TestHelper.basicChannel.id), root_id: post1.id} + ); + const post2 = await Client.createPost( + TestHelper.basicTeam.id, + TestHelper.fakePost(TestHelper.basicChannel.id) + ); + const post3 = await Client.createPost( + TestHelper.basicTeam.id, + TestHelper.fakePost(TestHelper.basicChannel.id) + ); + const post3a = await Client.createPost( + TestHelper.basicTeam.id, + {...TestHelper.fakePost(TestHelper.basicChannel.id), root_id: post3.id} + ); + + store.subscribe(() => { + const getRequest = store.getState().requests.posts.getPosts; + const postsInfo = store.getState().entities.posts.postsInfo; + + if (getRequest.status === RequestStatus.SUCCESS || getRequest.status === RequestStatus.FAILURE) { + if (getRequest.error) { + done(new Error(JSON.stringify(getRequest.error))); + } else { + const postsList = postsInfo[TestHelper.basicChannel.id]; + assert.ok(postsList); + assert.equal(postsList.order[0], post3a.id, 'wrong order'); + assert.equal(postsList.order[1], post3.id, 'wrong order'); + assert.equal(postsList.order[3], post1a.id, 'wrong order'); + assert.ok(postsList.posts[post1.id], 'should exists'); + assert.ok(postsList.posts[post2.id], 'should exists'); + done(); + } + } + }); + + Actions.getPosts( + TestHelper.basicTeam.id, + TestHelper.basicChannel.id + )(store.dispatch, store.getState); + }); + }).timeout(3000); + + it('getPostsSince', (done) => { + TestHelper.initBasic(Client).then(async () => { + const store = configureStore(); + const post1 = await Client.createPost( + TestHelper.basicTeam.id, + TestHelper.fakePost(TestHelper.basicChannel.id) + ); + await Client.createPost( + TestHelper.basicTeam.id, + {...TestHelper.fakePost(TestHelper.basicChannel.id), root_id: post1.id} + ); + const post2 = await Client.createPost( + TestHelper.basicTeam.id, + TestHelper.fakePost(TestHelper.basicChannel.id) + ); + const post3 = await Client.createPost( + TestHelper.basicTeam.id, + TestHelper.fakePost(TestHelper.basicChannel.id) + ); + const post3a = await Client.createPost( + TestHelper.basicTeam.id, + {...TestHelper.fakePost(TestHelper.basicChannel.id), root_id: post3.id} + ); + + store.subscribe(() => { + const getRequest = store.getState().requests.posts.getPostsSince; + const postsInfo = store.getState().entities.posts.postsInfo; + + if (getRequest.status === RequestStatus.SUCCESS || getRequest.status === RequestStatus.FAILURE) { + if (getRequest.error) { + done(new Error(JSON.stringify(getRequest.error))); + } else { + const postsList = postsInfo[TestHelper.basicChannel.id]; + assert.ok(postsList); + assert.equal(postsList.order[0], post3a.id, 'wrong order'); + assert.equal(postsList.order[1], post3.id, 'wrong order'); + assert.equal(Object.keys(postsList.posts).length, 2, 'wrong size'); + done(); + } + } + }); + + Actions.getPostsSince( + TestHelper.basicTeam.id, + TestHelper.basicChannel.id, + post2.create_at + )(store.dispatch, store.getState); + }); + }).timeout(3000); + + it('getPostsBefore', (done) => { + TestHelper.initBasic(Client).then(async () => { + const store = configureStore(); + const post1 = await Client.createPost( + TestHelper.basicTeam.id, + TestHelper.fakePost(TestHelper.basicChannel.id) + ); + const post1a = await Client.createPost( + TestHelper.basicTeam.id, + {...TestHelper.fakePost(TestHelper.basicChannel.id), root_id: post1.id} + ); + const post2 = await Client.createPost( + TestHelper.basicTeam.id, + TestHelper.fakePost(TestHelper.basicChannel.id) + ); + const post3 = await Client.createPost( + TestHelper.basicTeam.id, + TestHelper.fakePost(TestHelper.basicChannel.id) + ); + await Client.createPost( + TestHelper.basicTeam.id, + {...TestHelper.fakePost(TestHelper.basicChannel.id), root_id: post3.id} + ); + + store.subscribe(() => { + const getRequest = store.getState().requests.posts.getPostsBefore; + const postsInfo = store.getState().entities.posts.postsInfo; + + if (getRequest.status === RequestStatus.SUCCESS || getRequest.status === RequestStatus.FAILURE) { + if (getRequest.error) { + done(new Error(JSON.stringify(getRequest.error))); + } else { + const postsList = postsInfo[TestHelper.basicChannel.id]; + assert.ok(postsList); + assert.equal(postsList.order[0], post1a.id, 'wrong order'); + assert.equal(postsList.order[1], post1.id, 'wrong order'); + assert.equal(Object.keys(postsList.posts).length, 3, 'wrong size'); + done(); + } + } + }); + + Actions.getPostsBefore( + TestHelper.basicTeam.id, + TestHelper.basicChannel.id, + post2.id, + 0, + 10 + )(store.dispatch, store.getState); + }); + }).timeout(3000); + + it('getPostsAfter', (done) => { + TestHelper.initBasic(Client).then(async () => { + const store = configureStore(); + const post1 = await Client.createPost( + TestHelper.basicTeam.id, + TestHelper.fakePost(TestHelper.basicChannel.id) + ); + await Client.createPost( + TestHelper.basicTeam.id, + {...TestHelper.fakePost(TestHelper.basicChannel.id), root_id: post1.id} + ); + const post2 = await Client.createPost( + TestHelper.basicTeam.id, + TestHelper.fakePost(TestHelper.basicChannel.id) + ); + const post3 = await Client.createPost( + TestHelper.basicTeam.id, + TestHelper.fakePost(TestHelper.basicChannel.id) + ); + const post3a = await Client.createPost( + TestHelper.basicTeam.id, + {...TestHelper.fakePost(TestHelper.basicChannel.id), root_id: post3.id} + ); + + store.subscribe(() => { + const getRequest = store.getState().requests.posts.getPostsAfter; + const postsInfo = store.getState().entities.posts.postsInfo; + + if (getRequest.status === RequestStatus.SUCCESS || getRequest.status === RequestStatus.FAILURE) { + if (getRequest.error) { + done(new Error(JSON.stringify(getRequest.error))); + } else { + const postsList = postsInfo[TestHelper.basicChannel.id]; + assert.ok(postsList); + assert.equal(postsList.order[0], post3a.id, 'wrong order'); + assert.equal(postsList.order[1], post3.id, 'wrong order'); + assert.equal(Object.keys(postsList.posts).length, 2, 'wrong size'); + done(); + } + } + }); + + Actions.getPostsAfter( + TestHelper.basicTeam.id, + TestHelper.basicChannel.id, + post2.id, + 0, + 10 + )(store.dispatch, store.getState); + }); + }).timeout(3000); +}); diff --git a/test/service/actions/users.test.js b/test/service/actions/users.test.js index 6a3650fdf..bf8d99aa7 100644 --- a/test/service/actions/users.test.js +++ b/test/service/actions/users.test.js @@ -96,7 +96,6 @@ describe('Actions.Users', () => { assert.strictEqual(posts.selectedPostId, '', 'selected post id is not empty'); assert.strictEqual(posts.currentFocusedPostId, '', 'current focused post id is not empty'); assert.deepStrictEqual(posts.postsInfo, {}, 'posts info is not empty'); - assert.deepStrictEqual(posts.latestPageTime, {}, 'posts latest page time is not empty'); assert.deepStrictEqual(preferences.myPreferences, {}, 'user preferences not empty'); assert.strictEqual(navigation.index, 0, 'navigation not reset to first element of stack'); assert.deepStrictEqual(navigation.routes, [Routes.Root], 'navigation not reset to root route');