import imageInputRESTFormatter from '../../util/imageInputRESTFormatter';
import PostType from '../../data/enum/PostType';
import SortType from '../../data/enum/SortType';
import { convertBitwiseToArray } from '../../util/bitwiseUtils';
import GatewayError from '../../net/gateway/GatewayError';
import isAdministratorOrModerator from '../../util/userRoleUtil';
import { userIdSelector } from '../../selectors/userAccountSelectors';
import {
GATEWAY_COMMUNITY_AUTH,
GATEWAY_COMMUNITY_V2_AUTH,
GATEWAY_CONTENT_AUTH,
} from '../../data/Injectables';
import { COMMENT, POST, TAG, COMMUNITY_GROUP } from '../../data/entityTypes';
import dedupeAsyncThunk from '../../util/dedupeAsyncThunk';
import { removeEntities, setEntity } from '../entities/entityActions';
import { apiDelete, apiPatch, apiPost, apiGet } from './apiActions/apiRequest';
import getDefaultApiEntityTransform from './apiActions/getDefaultApiEntityTransform';
import apiGetEntity from './apiActions/apiGetEntity';
import apiGetCollection, {
collectionCachingNoPagination,
addEntitiesFromApiData,
GET_NEXT,
} from './apiActions/apiGetCollection';
import {
groupPostsCollectionId,
communityCommentsCollectionId,
communityPostsCollectionId,
communityRelatedPostsCollectionId,
communityGroupPostsCollectionId,
COMMUNITY_TAGS,
COMMUNITY_GROUPS,
COMMUNITY_MEMBER_GROUPS,
} from '../../data/collectionIds';
import { POST_OVERVIEW, COMMUNITY_GROUP_POSTS } from '../../data/collectionPaginationViews';
import {
collectionClearAll,
collectionClearIfContainsRef,
collectionPartitionCheckComplete,
collectionRemoveRefs,
} from '../entities/collectionActions';
import getGroupId from '../../util/getGroupId';
import authenticate from '../../util/auth/authenticate';
import { getUserPermissionStoredData } from '../../util/userPermissionStateUtil';
export const GET_COMMUNITY_FEED = 'communityActions/GET_COMMUNITY_FEED';
export const getCommunityFeed = (
{
sort,
includePinnedFirst = false,
loadMore = true,
includeLastLike = true,
limit = 10,
useCursorPagination = true,
},
getFresh = false,
) => async (dispatch, getState) => {
const { requestData, requestPagination } = buildPostFilters({
sort,
includePinnedFirst,
loadMore,
includeLastLike,
limit,
useCursorPagination,
});
await authenticate();
const userId = userIdSelector(getState());
return dispatch(
apiGetCollection(
GET_COMMUNITY_FEED,
GATEWAY_COMMUNITY_AUTH,
`/profiles/${userId}/community-feed`,
communityPostsCollectionId({ sort }),
requestPagination,
{
updatePaginationView: {
target: POST_OVERVIEW,
extend: true,
},
requestData,
entityType: POST,
caching: getFresh ? false : undefined,
transformResponse: response => {
// eslint-disable-next-line no-unused-vars
const { total: removeTotal, ...paginationWithoutTotal } = response.pagination;
return {
...response,
pagination: {
...paginationWithoutTotal,
},
};
},
transformEntity: transformNormalizePost,
},
),
).then(({ result }) => {
dispatch(
addCommunityGroupEntitiesFromPosts(
result.data,
getDefaultApiEntityTransform(`/profiles/${userId}/community-feed`, GET_COMMUNITY_FEED),
),
);
});
};
export const GET_POSTS_OVERVIEW = 'communityActions/GET_POSTS_OVERVIEW';
export const getPostsOverview = (
{
tags = 0,
onlyMe,
sort,
postType,
isDeleted = false,
includePinnedFirst = false,
loadMore = true,
includeTrending,
includeLastLike = true,
limit = 10,
useCursorPagination = true,
},
containerId = null,
getFresh = false,
) => dispatch => {
const { requestData, requestPagination } = buildPostFilters({
tags,
onlyMe,
sort,
postType,
isDeleted,
includePinnedFirst,
loadMore,
includeTrending,
includeLastLike,
limit,
useCursorPagination,
containerId,
});
return dispatch(
apiGetCollection(
GET_POSTS_OVERVIEW,
GATEWAY_COMMUNITY_V2_AUTH,
'/posts',
communityPostsCollectionId({ tags, onlyCurrentUser: onlyMe, postType, containerId, sort }),
requestPagination,
{
updatePaginationView: {
target: POST_OVERVIEW,
extend: true,
},
requestData,
entityType: POST,
caching: getFresh ? false : undefined,
transformResponse: response => {
// eslint-disable-next-line no-unused-vars
const { total: removeTotal, ...paginationWithoutTotal } = response.pagination;
return {
...response,
pagination: {
...paginationWithoutTotal,
},
};
},
transformEntity: transformNormalizePost,
},
),
).then(({ result }) => {
dispatch(
addCommunityGroupEntitiesFromPosts(
result.data,
getDefaultApiEntityTransform(`/posts`, GET_POSTS_OVERVIEW),
),
);
});
};
export const GET_POST_DETAIL = 'communityActions/GET_POST_DETAIL';
/**
* Gets a single post entity from the API
* @param id {string} The ID of the post to get
*/
export const getPostDetail = id => dispatch =>
dispatch(
apiGetEntity(GET_POST_DETAIL, GATEWAY_COMMUNITY_V2_AUTH, `/posts/${id}`, POST, id, {
updateEntityView: 'view.pages.communityDetail.post',
requestData: {
includeLastLike: true,
},
caching: false,
transformEntity: transformNormalizePost,
}),
).then(response => {
if (response.entity.communityGroup) {
dispatch(
addEntitiesFromApiData(
[response.entity.communityGroup],
COMMUNITY_GROUP,
undefined,
getDefaultApiEntityTransform(`/posts/${id}`, GET_POST_DETAIL),
),
);
}
return response;
});
export const GET_RELATED_POSTS = 'communityActions/GET_RELATED_POSTS';
export const getRelatedPosts = postId => dispatch =>
dispatch(
apiGetCollection(
GET_RELATED_POSTS,
GATEWAY_COMMUNITY_AUTH,
`/posts/${postId}/related`,
communityRelatedPostsCollectionId({ postId }),
{
limit: 4,
offset: 0,
},
{
entityType: POST,
transformEntity: transformNormalizePost,
},
),
).then(({ result }) => {
dispatch(
addCommunityGroupEntitiesFromPosts(
result.data,
getDefaultApiEntityTransform(`/posts/${postId}/related`, GET_RELATED_POSTS),
),
);
});
export const GET_GROUP_POSTS = 'communityActions/GET_GROUP_POSTS';
export const getGroupPosts = (containerId = null, limit = 6) => async (dispatch, getState) => {
// eslint-disable-next-line no-param-reassign
containerId = await getGroupId(containerId, getState);
return dispatch(
apiGetCollection(
GET_GROUP_POSTS,
GATEWAY_COMMUNITY_AUTH,
'/posts',
groupPostsCollectionId({ containerId }),
{
limit,
},
{
useCache: true,
entityType: POST,
requestData: {
postType: PostType.POST,
sort: SortType.NEWEST,
containerId,
},
},
),
);
};
export const GET_DISCUSSION_COMMENTS = 'communityActions/GET_DISCUSSION_COMMENTS';
export const getDiscussionComments = (id, isComment = false, loadMore = false) =>
apiGetCollection(
GET_DISCUSSION_COMMENTS,
GATEWAY_COMMUNITY_V2_AUTH,
'/comments',
communityCommentsCollectionId({ parentId: id, parentIsComment: isComment }),
{
limit: 10,
offset: loadMore ? GET_NEXT : 0,
},
{
requestData: {
[isComment ? 'parentId' : 'discussionId']: id,
sort: isComment ? SortType.OLDEST : SortType.NEWEST,
},
entityType: COMMENT,
},
);
export const getDiscussionCommentsForDeeplink = (
id,
isComment = false,
deeplink,
{ next = false, previous = false } = {},
) => (dispatch, getState) => {
const collectionId = communityCommentsCollectionId({
parentId: id,
parentIsComment: isComment,
deeplink: true,
});
let paginationOptions = isComment
? { from: { param: 'sinceId', value: deeplink, key: 'id' } }
: { until: { param: 'untilId', value: deeplink, key: 'id' } };
if (next) {
if (previous) {
throw new Error(
'The next and previous options of getDiscussionCommentsForDeeplink are mutually exclusive',
);
}
paginationOptions = { from: { param: 'sinceId', key: 'id' } };
} else if (previous) {
paginationOptions = { until: { param: 'untilId', key: 'id' } };
}
return dispatch(
apiGetCollection(
GET_DISCUSSION_COMMENTS,
GATEWAY_COMMUNITY_V2_AUTH,
'/comments',
collectionId,
{
...paginationOptions,
limit: 10,
},
{
requestData: {
[isComment ? 'parentId' : 'discussionId']: id,
sort: isComment ? SortType.OLDEST : SortType.NEWEST,
},
entityType: COMMENT,
},
),
).then(result => {
const { entities } = getState();
if (!next && !previous) {
const discussionEntityType = isComment ? 'comment' : 'post';
const discussionTotalProp = isComment ? 'repliesCount' : 'commentsCount';
const total = entities[discussionEntityType]?.[id]?.[discussionTotalProp];
if (typeof total === 'number') {
dispatch(collectionPartitionCheckComplete(collectionId, total));
}
}
return result;
});
};
export const GET_COMMUNITY_TAGS = 'communityActions/GET_COMMUNITY_TAGS';
export const getCommunityTags = dedupeAsyncThunk(() => async (dispatch, getState) => {
await authenticate();
const adminOrModerator = isAdministratorOrModerator(
getUserPermissionStoredData({ getState }).roles,
);
return dispatch(
apiGetCollection(
GET_COMMUNITY_TAGS,
GATEWAY_CONTENT_AUTH,
'/community-post-tags',
COMMUNITY_TAGS,
{},
{
entityType: TAG,
// simplified caching. If we have any tags, abort the call
caching: collectionCachingNoPagination,
// If the member is not a moderator or administrator, remove any moderatorOnly tags
transformResponse: response => {
const apiEntity = adminOrModerator
? response.data
: response.data.filter(tag => tag.moderatorOnly === adminOrModerator);
return {
data: apiEntity,
};
},
},
),
);
});
export const CREATE_COMMUNITY_POST = 'communityActions/CREATE_COMMUNITY_POST';
/**
* Creates a new post in the community
* @param title {string} The title of the post
* @param text {string} The text content of the post
* @param tags {Array<number>} An array of tag ids to attach to the post
* @param images {Array<string>} An array of base64 encoded image strings
* @param communityGroupId {string} (Optional) The member group id which the post is associated to
* @param containerId {string} (Optional) The groups id
*/
export const createCommunityPost = (
title,
text,
tags,
images,
communityGroupId,
containerId,
) => dispatch =>
dispatch(
apiPost(CREATE_COMMUNITY_POST, GATEWAY_COMMUNITY_V2_AUTH, '/posts', {
title,
text,
tags,
media: imageInputRESTFormatter(images),
communityGroupId,
containerId,
}),
).then(data => {
dispatch(collectionClearAll(communityPostsCollectionId()));
dispatch(collectionClearAll(communityGroupPostsCollectionId()));
return data;
});
export const EDIT_COMMUNITY_POST = 'communityActions/EDIT_COMMUNITY_POST';
export const editCommunityPost = (previousPost, title, text, tags, images) => dispatch => {
const newImage = (images && images[0]) || null;
const oldImage = (previousPost && previousPost.media && previousPost.media.content) || null;
const data = { title, text, tags };
if (newImage !== oldImage) {
if (newImage) {
data.media = imageInputRESTFormatter(images);
} else {
data.deleteMedia = true;
}
}
return dispatch(
apiPatch(EDIT_COMMUNITY_POST, GATEWAY_COMMUNITY_V2_AUTH, `/posts/${previousPost.id}`, data),
).then(({ data: { media, mentions } }) =>
dispatch(
setEntity(
POST,
previousPost.id,
{
tags: (tags || []).map(tag => (typeof tag === 'number' ? tag : parseInt(tag, 10))),
title,
text,
media,
mentions,
},
true,
),
),
);
};
export const CREATE_COMMUNITY_COMMENT = 'communityActions/CREATE_COMMUNITY_COMMENT';
export const createCommunityComment = (
discussionId,
discussionTitle,
discussionKind,
message,
parentId = null,
images = [],
) => dispatch =>
dispatch(
apiPost(CREATE_COMMUNITY_COMMENT, GATEWAY_COMMUNITY_V2_AUTH, '/comments', {
message,
discussionKind,
discussionId,
discussionTitle,
...(parentId ? { parentId } : {}),
media: imageInputRESTFormatter(images),
}),
);
export const EDIT_COMMUNITY_COMMENT = 'communityActions/EDIT_COMMUNITY_COMMENT';
export const editCommunityComment = (id, previousMedia, message, images = []) => dispatch => {
const newImage = (images && images[0]) || null;
const oldImage = (previousMedia && previousMedia.content) || null;
const data = { message };
if (newImage !== oldImage) {
if (newImage) {
data.media = imageInputRESTFormatter(images);
} else {
data.deleteMedia = true;
}
}
return dispatch(
apiPatch(EDIT_COMMUNITY_COMMENT, GATEWAY_COMMUNITY_V2_AUTH, `/comments/${id}`, data),
).then(({ data: { media, mentions } }) =>
dispatch(
setEntity(
COMMENT,
id,
{
message,
media,
mentions,
},
true,
),
),
);
};
export const REPORT_POST = 'communityActions/REPORT_POST';
export const reportPost = id =>
apiPost(REPORT_POST, GATEWAY_COMMUNITY_AUTH, `/posts/${id}/reports`);
export const DELETE_POST = 'communityActions/DELETE_POST';
export const deletePost = id => dispatch =>
dispatch(apiDelete(DELETE_POST, GATEWAY_COMMUNITY_AUTH, `/posts/${id}`)).then(() => {
/*
It's getting the same post to check
if the comment has been deleted and update it
*/
dispatch(apiGet(GET_POST_DETAIL, GATEWAY_COMMUNITY_AUTH, `/posts/${id}`))
.then(result => {
dispatch(setEntity(POST, id, result.data, true));
})
.catch(e => {
// In case 404 it should be removed from collections and entities
if (e instanceof GatewayError && e.response.status === 404) {
// remove post from community overview
dispatch(collectionRemoveRefs(communityPostsCollectionId(), [{ id, type: POST }]));
// remove post from related posts
dispatch(
collectionClearIfContainsRef(communityRelatedPostsCollectionId(), [{ id, type: POST }]),
);
dispatch(removeEntities([id], POST));
}
});
});
export const REPORT_COMMENT = 'communityActions/REPORT_COMMENT';
export const reportComment = id =>
apiPost(REPORT_COMMENT, GATEWAY_COMMUNITY_AUTH, `/comments/${id}/reports`);
export const DELETE_COMMENT = 'communityActions/DELETE_COMMENT';
export const GET_COMMENT = 'communityActions/GET_COMMENT';
export const deleteComment = (id, discussionId, isReply, parentId) => dispatch =>
dispatch(apiDelete(DELETE_COMMENT, GATEWAY_COMMUNITY_AUTH, `/comments/${id}`)).then(() => {
/*
It's getting the same comment to check
if the comment has been deleted and update it
*/
dispatch(apiGet(GET_COMMENT, GATEWAY_COMMUNITY_AUTH, `/comments/${id}`))
.then(result => {
dispatch(setEntity(COMMENT, id, result.data, true));
})
.catch(e => {
// In case 404 It should be removed from collections and entities
if (e instanceof GatewayError && e.response.status === 404) {
// remove comment from community/post overview
dispatch(
collectionRemoveRefs(
communityCommentsCollectionId({
parentId: isReply ? parentId : discussionId,
parentIsComment: isReply,
}),
[{ id, type: COMMENT }],
),
);
dispatch(removeEntities([id], COMMENT));
if (!isReply) {
dispatch(updateCommentsCountPost(discussionId, -1));
}
}
});
});
export const updateCommentsCountPost = (postId, count) => (dispatch, getState) => {
const state = getState();
const posts = state.entities[POST];
const post = posts ? posts[postId] : null;
if (post) {
const commentsCount = (post.commentsCount || 0) + count;
return dispatch(setEntity(POST, postId, { commentsCount }, true));
}
return {};
};
export const GET_COMMUNITY_USERNAMES = 'communityActions/GET_COMMUNITY_USERNAMES';
export const getCommunityMentions = username => dispatch =>
dispatch(
apiGet(GET_COMMUNITY_USERNAMES, GATEWAY_COMMUNITY_AUTH, `/mentions?userName=${username}`),
).then(result => {
// Map data into correct shape for draft-js mention component
const members = result.data.map(member => ({
id: member.id,
name: member.userName,
avatar: member.avatar,
}));
return members;
});
export const GET_COMMUNITY_GROUP_POSTS = 'communityActions/GET_COMMUNITY_GROUP_POSTS';
export const getCommunityGroupPosts = (
slug,
{
tags = 0,
onlyMe,
sort,
postType,
isDeleted = false,
includeCommunityGroupPinnedFirst = false,
loadMore = true,
includeTrending,
includeLastLike = true,
limit = 10,
useCursorPagination = true,
},
containerId = null,
getFresh = false,
) => dispatch => {
const { requestData, requestPagination } = buildPostFilters({
tags,
onlyMe,
sort,
postType,
isDeleted,
includeCommunityGroupPinnedFirst,
loadMore,
includeTrending,
includeLastLike,
limit,
useCursorPagination,
containerId,
});
return dispatch(
apiGetCollection(
GET_COMMUNITY_GROUP_POSTS,
GATEWAY_COMMUNITY_AUTH,
`/community-groups/${slug}/posts`,
communityGroupPostsCollectionId({
slug,
tags,
onlyCurrentUser: onlyMe,
postType,
containerId,
sort,
}),
requestPagination,
{
updatePaginationView: {
target: COMMUNITY_GROUP_POSTS,
extend: true,
},
requestData,
entityType: POST,
caching: getFresh ? false : undefined,
transformResponse: response => {
// eslint-disable-next-line no-unused-vars
const { total: removeTotal, ...paginationWithoutTotal } = response.pagination;
return {
...response,
pagination: {
...paginationWithoutTotal,
},
};
},
transformEntity: transformNormalizePost,
},
),
).then(({ result }) => {
dispatch(
addCommunityGroupEntitiesFromPosts(
result.data,
getDefaultApiEntityTransform(`/community-groups/${slug}/posts`, GET_COMMUNITY_GROUP_POSTS),
),
);
});
};
export const GET_COMMUNITY_GROUP = 'communityActions/GET_COMMUNITY_GROUP';
export const getCommunityGroup = slug => async dispatch =>
dispatch(
apiGetEntity(
GET_COMMUNITY_GROUP,
GATEWAY_COMMUNITY_AUTH,
`/community-groups/${slug}`,
COMMUNITY_GROUP,
{
findEntity: entity => entity.slug === slug,
getId: entity => entity.id,
},
{},
),
);
export const JOIN_COMMUNITY_GROUP = 'communityActions/JOIN_COMMUNITY_GROUP';
export const joinCommunityGroup = communityGroupId => async (dispatch, getState) => {
await authenticate();
const userId = userIdSelector(getState());
dispatch(
apiPost(
JOIN_COMMUNITY_GROUP,
GATEWAY_COMMUNITY_AUTH,
`/profiles/${userId}/community-groups/${communityGroupId}`,
),
).then(() => {
// get current members count so we can increment it
const state = getState();
const { membersCount } = state.entities[COMMUNITY_GROUP][communityGroupId];
dispatch(
setEntity(
COMMUNITY_GROUP,
communityGroupId,
{ hasJoined: true, membersCount: membersCount + 1 },
true,
),
);
// clear community posts collection so the community feed is re-fetched
dispatch(collectionClearAll(communityPostsCollectionId()));
});
};
export const LEAVE_COMMUNITY_GROUP = 'communityActions/LEAVE_COMMUNITY_GROUP';
export const leaveCommunityGroup = communityGroupId => async (dispatch, getState) => {
await authenticate();
const userId = userIdSelector(getState());
dispatch(
apiDelete(
LEAVE_COMMUNITY_GROUP,
GATEWAY_COMMUNITY_AUTH,
`/profiles/${userId}/community-groups/${communityGroupId}`,
),
).then(() => {
// get current members count so we can decrement it
const state = getState();
const { membersCount } = state.entities[COMMUNITY_GROUP][communityGroupId];
dispatch(
setEntity(
COMMUNITY_GROUP,
communityGroupId,
{ hasJoined: false, membersCount: membersCount - 1 },
true,
),
);
// clear community posts collection so the community feed is re-fetched
dispatch(collectionClearAll(communityPostsCollectionId()));
});
};
export const GET_MEMBER_COMMUNITY_GROUPS = 'communityActions/GET_MEMBER_COMMUNITY_GROUPS';
export const getMemberCommunityGroups = () => async (dispatch, getState) => {
await authenticate();
const userId = userIdSelector(getState());
return dispatch(
apiGetCollection(
GET_MEMBER_COMMUNITY_GROUPS,
GATEWAY_COMMUNITY_AUTH,
`/profiles/${userId}/community-groups`,
COMMUNITY_MEMBER_GROUPS,
{},
{
useCache: false,
entityType: COMMUNITY_GROUP,
},
),
);
};
export const GET_ALL_COMMUNITY_GROUPS = 'communityActions/GET_ALL_COMMUNITY_GROUPS';
export const getCommunityGroups = () => async dispatch =>
dispatch(
apiGetCollection(
GET_ALL_COMMUNITY_GROUPS,
GATEWAY_COMMUNITY_AUTH,
'/community-groups',
COMMUNITY_GROUPS,
{},
{
entityType: COMMUNITY_GROUP,
},
),
);
const buildPostFilters = ({
tags = 0,
onlyMe,
sort,
postType,
isDeleted = false,
includePinnedFirst = false,
includeCommunityGroupPinnedFirst = false,
loadMore = true,
includeTrending,
includeLastLike = true,
limit = 10,
useCursorPagination = true,
containerId = null,
}) => {
const requestData = {
includeLastLike,
postType: postType || PostType.POST,
sort: sort || SortType.NEWEST,
includePinnedFirst,
includeCommunityGroupPinnedFirst,
isDeleted,
includeTrending,
};
if (onlyMe) {
requestData.onlyCurrentUser = true;
}
if (containerId) {
requestData.containerId = containerId;
}
if (tags) {
requestData.tags = convertBitwiseToArray(tags).join(',');
}
const requestPagination = { limit };
const sortArray = [SortType.OLDEST, SortType.LATEST_ACTIVITY_DATE, SortType.MOST_LIKED];
// only do cursor based pagination when sorting is on 'id'
if (
(requestData.sort.endsWith('id') && useCursorPagination) ||
sortArray.some(item => requestData.sort.endsWith(item))
) {
requestPagination.from = {
key: 'id',
param: 'sinceId',
};
if (requestData.sort.endsWith(SortType.LATEST_ACTIVITY_DATE)) {
requestPagination.andFrom = {
key: 'latestActivityDate',
param: 'sinceSortFieldValue',
};
} else if (requestData.sort.endsWith(SortType.MOST_LIKED)) {
requestPagination.andFrom = {
key: 'likesCount',
param: 'sinceSortFieldValue',
};
}
} else {
requestPagination.offset = loadMore ? GET_NEXT : 0;
}
return { requestData, requestPagination };
};
const transformNormalizePost = post => {
// eslint-disable-next-line no-unused-vars
const { communityGroup: remove, ...postNormalized } = post;
return {
communityGroupId: post?.communityGroup?.id || null,
...postNormalized,
};
};
const addCommunityGroupEntitiesFromPosts = (data, transformEntity) => dispatch => {
const entities = Array.isArray(data) ? data : [data];
const communityGroups = entities
.filter(
({ communityGroup }) => communityGroup !== null && typeof communityGroup !== 'undefined',
)
.map(({ communityGroup }) => communityGroup);
dispatch(addEntitiesFromApiData(communityGroups, COMMUNITY_GROUP, undefined, transformEntity));
};