@stackend/api
Version:
JS bindings to api.stackend.com
518 lines (468 loc) • 15.2 kB
text/typescript
// @flow
import get from 'lodash/get';
import update from 'immutability-helper';
import { Category } from '../category';
import { fetchMyGroups, receiveGroups, receiveGroupsAuth } from '../group/groupActions';
import { receiveBlogs } from './blogActions';
import { Group, listMyGroups } from '../group';
import { getJsonErrorText, newXcapJsonErrorResult, Thunk } from '../api';
import * as commentActions from '../comments/commentAction';
import * as commentApi from '../comments';
import { GetCommentResult, GetMultipleCommentsResult } from '../comments';
import {
getGroupBlogState,
GroupBlogEntriesActions,
GroupBlogEntriesState,
GroupBlogState,
hasBlogEntries,
INVALIDATE_GROUP_BLOG_ENTRIES,
RECEIVE_GROUP_BLOG_ENTRIES,
REQUEST_GROUP_BLOG_ENTRIES,
UPDATE_GROUP_BLOG_ENTRY,
UpdateBlogEntry
} from './groupBlogEntriesReducer';
import {
BlogEntry,
GetBlogEntryResult,
getCompositeBlogKey,
getEntries,
GetEntriesResult,
getEntry,
SaveBlogEntryInput,
saveEntry,
SaveEntryResult,
SetEntryStatus,
setEntryStatus
} from './index';
import { receiveLikes } from '../like/likeActions';
import { PaginatedCollection } from '../api/PaginatedCollection';
import { eventsReceived } from '../event/eventActions';
import { updatePoll, updatePolls } from '../poll/pollActions';
import { AuthObject } from '../user/privileges';
import { Poll } from '../poll';
//import { sendEventToGA } from '../analytics/analyticsFunctions.js';
/**
* Load comments in a group and for a specific blogEntry
*
* @since 15 fen 2017
* @author pelle
*/
/**
* When loading comments receive is run when the server has responded
* @param blogKey
* @param json
*/
export function receiveBlogEntries(blogKey: string, json: GetEntriesResult): GroupBlogEntriesActions {
return {
type: RECEIVE_GROUP_BLOG_ENTRIES,
blogKey,
json,
receivedAt: Date.now()
};
}
/**
* Discard cached blog entries
* @param blogKey
*/
export function cleanCacheBlogEntries({ blogKey }: { blogKey: string }): GroupBlogEntriesActions {
return {
type: INVALIDATE_GROUP_BLOG_ENTRIES,
blogKey
};
}
/**
* Request comments from the server
* @param blogKey
*/
export function requestBlogEntries(blogKey: string): GroupBlogEntriesActions {
return {
type: REQUEST_GROUP_BLOG_ENTRIES,
blogKey
};
}
/**
* Update already existing blog entry.
* Only the last BlogEntry of json.resultPaginated.entries will be updated.
*/
export function updateBlogEntry(blogKey: string, json: UpdateBlogEntry): GroupBlogEntriesActions {
return {
type: UPDATE_GROUP_BLOG_ENTRY,
blogKey,
json,
receivedAt: Date.now()
};
}
export interface FetchBlogEntries {
blogKey: string; //The id of the blogKey that you want to store the data in redux
pageSize?: number;
p?: number; //page number in paginated collection
categories?: Array<Category>;
invalidatePrevious?: boolean; //if true, invalidates previous blog-entries in this blog,
goToBlogEntry?: string; // Start the pagination at the specified entry permalink
tags?: string[]; // Limit the listing to entries with the supplied tags
}
/**
* Requests and receive blog entries and store them in redux-state
*/
export function fetchBlogEntries({
blogKey,
pageSize = 15,
p = 1,
categories,
invalidatePrevious = false,
goToBlogEntry,
tags
}: FetchBlogEntries): Thunk<Promise<GetEntriesResult | null>> {
return async (dispatch: any, getState): Promise<GetEntriesResult | null> => {
const categoryId = get(categories, '[0].id', undefined);
const compositeBlogKey = getCompositeBlogKey({ blogKey, tags });
try {
const { currentUser, groups, groupBlogEntries } = getState();
// FIXME: Really list groups here?
const auth = get(groups, 'auth', {});
try {
if (get(currentUser, 'isLoggedIn', false) && (auth == null || Object.keys(auth).length === 0)) {
await dispatch(fetchMyGroups());
}
} catch (e) {
console.error("Couldn't receiveGroupsAuth in fetchBlogEntries for " + blogKey + ': ', e);
}
// Ignore if already present
if (
hasBlogEntries(groupBlogEntries, compositeBlogKey, pageSize, p, categoryId, goToBlogEntry) &&
!invalidatePrevious
) {
return null;
}
dispatch(requestBlogEntries(compositeBlogKey));
// FIXME: Category id not used in key?
const result: GetEntriesResult = await dispatch(
getEntries({ blogKey, pageSize, p, categoryId, goToBlogEntry, tags })
);
if (result.error) {
return result;
}
// FIXME: this should use the blog object returned by the above call, because this fails if there are no entries
const group = result.blog?.groupRef;
if (group) {
dispatch(receiveGroups({ entries: [group] }));
} else {
console.error("Couldn't receiveGroups in fetchBlogEntries for " + blogKey + '. Entries: ', result);
}
const blog = result.blog;
if (blog) {
const authObjects: { [blogId: number]: AuthObject } = {};
// FIXME: Workaround for different return types
if (result.authBlog) {
if (result.authBlog.auth) {
authObjects[blog.id] = result.authBlog.auth;
} else {
authObjects[blog.id] = result.authBlog as any;
}
}
await dispatch(
receiveBlogs({
entries: [blog],
authBlogs: result.authBlog ? [result.authBlog] : undefined,
authObjects
})
);
} else {
console.error("Couldn't receiveBlogs in fetchBlogEntries for " + blogKey + '. Entries: ', result);
}
if (invalidatePrevious) {
dispatch(cleanCacheBlogEntries({ blogKey: compositeBlogKey }));
}
dispatch(receiveLikes(result.likes));
dispatch(
eventsReceived({
relatedObjects: result.__relatedObjects,
rsvpUserIds: result.rsvpUserIds,
currentUserRsvpStatuses: result.userRsvpStatuses
})
);
const polls: Array<Poll> = [];
result.resultPaginated.entries.forEach((e: BlogEntry) => {
if (e.pollRef) {
polls.push(e.pollRef);
}
});
dispatch(updatePolls(polls));
dispatch(receiveBlogEntries(compositeBlogKey, result));
return result;
} catch (e) {
console.error("Couldn't fetchBlogEntries for " + blogKey + ': ', e);
return newXcapJsonErrorResult<GetEntriesResult>("Couldn't fetchBlogEntries for " + blogKey);
}
};
}
/**
* Requests and receive entries and store them in redux-state
*/
export function fetchBlogEntry({
id,
permalink,
blogKey
}: {
id?: number;
permalink?: string;
blogKey: string;
}): Thunk<Promise<GetBlogEntryResult | null>> {
return async (dispatch: any, getState): Promise<GetBlogEntryResult | null> => {
// FIXME: error handling
try {
await dispatch(requestBlogEntries(blogKey));
const { currentUser, groups } = getState();
const auth = groups?.auth || null;
if (currentUser?.isLoggedIn && (auth == null || Object.keys(auth).length === 0)) {
const r = await dispatch(listMyGroups({}));
await dispatch(receiveGroupsAuth({ entries: r.groupAuth }));
}
const response: GetBlogEntryResult = await dispatch(getEntry({ id, entryPermaLink: permalink, blogKey }));
if (response.error) {
return response;
}
dispatch(receiveLikes(response.likes));
dispatch(
eventsReceived({
relatedObjects: response.__relatedObjects
})
);
if (response.blogEntry) {
dispatch(updatePoll(response.blogEntry.pollRef));
}
await dispatch(_fetchBlogEntry(blogKey, response));
return response;
} catch (e) {
console.log('Error fetchBlogEntry ' + (id ? id : permalink ? permalink : '') + ' from ' + blogKey + ':', e);
return null;
}
};
}
export interface FetchBlogEntriesWithComments {
blogKey: string;
page?: number;
categories?: Array<Category>;
goToBlogEntry?: string;
invalidatePrevious?: boolean;
tags?: string[];
}
export interface FetchBlogEntriesWithCommentsResult {
fetchBlogEntries: GetEntriesResult | null;
fetchMultipleComments: GetMultipleCommentsResult | null;
}
/**
* Fetch blog entries and their comments
* @param blogKey
* @param page
* @param categories
* @param goToBlogEntry
* @param invalidatePrevious
*/
export function fetchBlogEntriesWithComments({
blogKey,
page = 1,
categories,
goToBlogEntry,
invalidatePrevious = false,
tags
}: FetchBlogEntriesWithComments): Thunk<Promise<FetchBlogEntriesWithCommentsResult>> {
return async (dispatch: any): Promise<FetchBlogEntriesWithCommentsResult> => {
let blogResponse: GetEntriesResult | null = null;
let commentResponse: GetMultipleCommentsResult | null = null;
try {
blogResponse = await dispatch(
fetchBlogEntries({ blogKey, p: page, categories, goToBlogEntry, invalidatePrevious, tags })
);
if (blogResponse) {
if (blogResponse.error) {
console.error(
'Could not get blog entries for ' + blogKey + ': ',
blogResponse ? getJsonErrorText(blogResponse) : ''
);
return {
fetchBlogEntries: blogResponse,
fetchMultipleComments: null
};
}
const referenceIds = blogResponse.resultPaginated.entries.map((entry: BlogEntry) => entry.id);
commentResponse = dispatch(
commentActions.fetchMultipleComments({
module: commentApi.CommentModule.BLOG,
referenceIds,
referenceGroupId: blogResponse.blogId
})
);
} else {
// No result, probably not needed cause already in store
return {
fetchBlogEntries: null,
fetchMultipleComments: null
};
}
} catch (e) {
//FIXME: Probably a private group but need fail-check
console.error("Couldn't fetchBlogEntriesWithComments for " + blogKey + ': ', blogResponse, commentResponse, e);
}
return {
fetchBlogEntries: blogResponse,
fetchMultipleComments: commentResponse
};
};
}
export interface FetchBlogEntryWithCommentsResult {
fetchBlogEntry: GetBlogEntryResult | null;
fetchComments: GetCommentResult | null;
}
/**
* Fetch a blog entry and its comments
* @param id
* @param permalink
* @param blogKey
*/
export function fetchBlogEntryWithComments({
id,
permalink,
blogKey
}: {
id?: number;
permalink?: string;
blogKey: string;
}): Thunk<Promise<FetchBlogEntryWithCommentsResult>> {
return async (dispatch: any): Promise<FetchBlogEntryWithCommentsResult> => {
let blogResponse: GetBlogEntryResult | null = null;
let commentsResponse: GetCommentResult | null = null;
try {
blogResponse = await dispatch(fetchBlogEntry({ id, permalink, blogKey }));
if (!blogResponse) {
return { fetchBlogEntry: null, fetchComments: null };
}
if (!blogResponse.error) {
dispatch(
eventsReceived({
relatedObjects: blogResponse.__relatedObjects
})
);
dispatch(updatePoll(blogResponse.blogEntry?.pollRef));
}
const blogEntry = blogResponse.blogEntry;
if (blogEntry) {
dispatch(receiveLikes(blogResponse.likes));
commentsResponse = dispatch(
commentActions.fetchComments({
module: commentApi.CommentModule.BLOG,
referenceId: blogEntry.id,
referenceGroupId: blogEntry.blogId
})
);
} else {
console.warn(
'No such blog entry. id=' +
(id ? id : '?') +
', permalink=' +
(permalink ? permalink : '?') +
', blogKey=' +
blogKey
);
}
} catch (e) {
//FIXME: Probably a private group but need fail-check
console.warn(e);
}
return {
fetchBlogEntry: blogResponse,
fetchComments: commentsResponse
};
};
}
function _fetchBlogEntry(blogKey: string, json: GetBlogEntryResult): Thunk<any> {
return (dispatch: any): any => {
const groupRef: Group | null = json.blog?.groupRef || json.blogEntry?.blogRef?.groupRef;
if (groupRef) {
dispatch(receiveGroups({ entries: [groupRef] }));
}
const blog = json.blog || json.blogEntry?.blogRef;
if (blog) {
dispatch(receiveBlogs({ entries: [blog], authBlogs: json.authBlog ? [json.authBlog] : undefined }));
}
const resultPaginated = { entries: [json.blogEntry] } as PaginatedCollection<BlogEntry>;
return dispatch(
receiveBlogEntries(blogKey, {
resultPaginated,
likes: json.likes
} as GetEntriesResult)
);
};
}
/**
* Edit or create a blog entry.
*
* @param blogEntryInput
* @param type
* @param draftId
* @param blogKey
*/
export function postBlogEntry({
blogEntryInput,
type,
draftId,
blogKey
}: {
blogEntryInput: SaveBlogEntryInput;
type: 'PUBLISHED' | '';
blogKey: string; //The id of the blogKey that you want to store the data in redux
draftId?: number;
}): Thunk<Promise<SaveEntryResult>> {
return async (dispatch: any, getState): Promise<SaveEntryResult> => {
dispatch(requestBlogEntries(blogKey));
const response: SaveEntryResult = await dispatch(
saveEntry({
blogEntryInput,
type,
draftId,
blogKey
})
);
if (response.error) {
return response;
}
//In order to keep pagination-object we need to merge with current state
const { groupBlogEntries }: { groupBlogEntries: GroupBlogEntriesState } = getState() as any;
const groupBlogState: GroupBlogState | null = getGroupBlogState(groupBlogEntries, blogKey);
const resultPaginated = update(get(groupBlogState, 'json.resultPaginated', {}), {
entries: { $push: [response.entry] }
});
// FIXME: Fake some data. should not be needed
const getEntriesResult: GetEntriesResult = {
resultPaginated
} as GetEntriesResult;
dispatch(
eventsReceived({
relatedObjects: response.__relatedObjects
})
);
dispatch(updatePoll(response.entry?.pollRef));
if (!!blogEntryInput.id && blogEntryInput.id > 0) {
//dispatch(sendEventToGA(gaEditPostEventObject({ blogEntry: response.entry })));
dispatch(updateBlogEntry(blogKey, getEntriesResult));
} else {
//Add new blogEntry
// FIXME: Re add ga
//dispatch(sendEventToGA(gaPostEventObject({ blogEntry: response.entry })));
dispatch(receiveBlogEntries(blogKey, getEntriesResult));
}
return response;
};
}
/**
* Set the {@link BlogEntryStatus} of an entry.
* @param blogKey
* @param id
* @param status
*/
export function changeBlogEntryStatus({ blogKey, id, status }: SetEntryStatus): Thunk<Promise<void>> {
return async (dispatch: any): Promise<void> => {
const response = await dispatch(setEntryStatus({ blogKey, id, status }));
dispatch(updateBlogEntry(blogKey, { resultPaginated: { entries: [response.entry] } }));
};
}