Files
remark42/frontend/app/store/comments/reducers.ts
T
Pavel MineevandUmputun 93fd445bd4 On-demand auth
* auth block was moved to comment form
* if comments on page read only auth form shows in old place
* comment value in form will be saved between refreshes (it is side effect form saving comment value between unauth and auth states)
2020-03-12 15:44:31 -05:00

210 lines
5.5 KiB
TypeScript

import { Node, Comment, CommentMode, Sorting } from '@app/common/types';
import { combineReducers } from 'redux';
import {
COMMENTS_SET,
COMMENTS_SET_ACTION,
COMMENT_MODE_SET,
COMMENT_MODE_SET_ACTION,
COMMENTS_APPEND_ACTION,
COMMENTS_APPEND,
COMMENTS_EDIT_ACTION,
COMMENTS_EDIT,
COMMENTS_PATCH,
COMMENTS_PATCH_ACTION,
COMMENTS_SET_SORT,
COMMENTS_SET_SORT_ACTION,
COMMENTS_REQUEST_FETCHING,
COMMENTS_REQUEST_SUCCESS,
COMMENTS_REQUEST_FAILURE,
COMMENTS_REQUEST_ACTIONS,
} from './types';
import { getPinnedComments, getInitialSort } from './utils';
import { cmpRef } from '@app/utils/cmpRef';
export const topComments = (
state: Comment['id'][] = [],
action: COMMENTS_SET_ACTION | COMMENTS_APPEND_ACTION
): Comment['id'][] => {
switch (action.type) {
case COMMENTS_SET: {
return cmpRef(
state,
action.comments.map(x => x.comment.id)
);
}
case COMMENTS_APPEND: {
if (action.comment.pid) return state;
return [action.comment.id, ...state];
}
default:
return state;
}
};
const reduceChildIds = (c: Record<Comment['id'], Comment['id'][]>, x: Node): Record<Comment['id'], Comment['id'][]> => {
if (!x.replies) return c;
if (!c[x.comment.id]) {
c[x.comment.id] = [];
}
for (const reply of x.replies) {
c[x.comment.id].push(reply.comment.id);
if (reply.replies) {
reduceChildIds(c, reply);
}
}
return c;
};
export const childComments = (
state: Record<Comment['id'], Comment['id'][]> = {},
action: COMMENTS_SET_ACTION | COMMENTS_APPEND_ACTION
): Record<Comment['id'], Comment['id'][]> => {
switch (action.type) {
case COMMENTS_SET: {
return action.comments.reduce<Record<Comment['id'], Comment['id'][]>>(reduceChildIds, {});
}
case COMMENTS_APPEND: {
if (!action.comment.pid) return state;
return { ...state, [action.comment.pid]: [action.comment.id, ...(state[action.comment.pid] || [])] };
}
default:
return state;
}
};
const cmpComment = (a: Comment | undefined, b: Comment): Comment => {
if (!a) return b;
if (a.id !== b.id) return b;
if (!a.edit) {
if (!b.edit) return a;
return b;
}
if (!b.edit) return b;
if (a.edit.time !== b.edit.time) return b;
return a;
};
const reduceComments = (c: Record<Comment['id'], Comment>, x: Node): Record<Comment['id'], Comment> => {
c[x.comment.id] = cmpComment(c[x.comment.id], x.comment);
if (x.replies) {
x.replies.reduce(reduceComments, c);
}
return c;
};
export const allComments = (
state: Record<Comment['id'], Comment> = {},
action: COMMENTS_SET_ACTION | COMMENTS_APPEND_ACTION | COMMENTS_EDIT_ACTION | COMMENTS_PATCH_ACTION
): Record<Comment['id'], Comment> => {
switch (action.type) {
case COMMENTS_SET: {
return action.comments.reduce<Record<Comment['id'], Comment>>(reduceComments, { ...state });
}
case COMMENTS_APPEND:
case COMMENTS_EDIT: {
return { ...state, [action.comment.id]: action.comment };
}
case COMMENTS_PATCH: {
let newState = state;
let changed = false;
const editObject = { summary: '', time: new Date().toISOString() };
for (const id of action.ids) {
if (!Object.prototype.hasOwnProperty.call(state, id)) continue;
if (!changed) {
changed = true;
newState = { ...newState };
}
newState[id] = { ...newState[id], edit: editObject, ...action.patch };
}
return newState;
}
default:
return state;
}
};
export type ActiveCommentState = null | { id: Comment['id']; state: CommentMode };
export const activeComment = (
state: ActiveCommentState = null,
action: COMMENT_MODE_SET_ACTION
): ActiveCommentState => {
switch (action.type) {
case COMMENT_MODE_SET: {
return action.mode;
}
default:
return state;
}
};
export const pinnedComments = (
state: Comment['id'][] = [],
action: COMMENTS_SET_ACTION | COMMENTS_EDIT_ACTION | COMMENTS_PATCH_ACTION
): Comment['id'][] => {
switch (action.type) {
case COMMENTS_SET: {
return getPinnedComments(action.comments).map(x => x.id);
}
case COMMENTS_EDIT: {
const index = state.indexOf(action.comment.id);
if (!action.comment.pin) {
if (index === -1) return state;
const newState = [...state];
newState.splice(index, 1);
return newState;
}
if (index !== -1) return state;
return [...state, action.comment.id];
}
case COMMENTS_PATCH: {
if (!Object.prototype.hasOwnProperty.call(action.patch, 'pin')) return state;
if (!action.patch.pin) {
return state.filter(x => action.ids.indexOf(x) === -1);
}
return [...state, ...action.ids].reduce<Comment['id'][]>((c, x) => {
if (c.indexOf(x) === -1) {
c.push(x);
}
return c;
}, []);
}
default:
return state;
}
};
function isFetching(state: boolean = false, action: COMMENTS_REQUEST_ACTIONS): boolean {
switch (action.type) {
case COMMENTS_REQUEST_FETCHING:
return true;
case COMMENTS_REQUEST_SUCCESS:
case COMMENTS_REQUEST_FAILURE:
return false;
default:
return state;
}
}
function sort(state: Sorting = getInitialSort(), action: COMMENTS_SET_SORT_ACTION): Sorting {
switch (action.type) {
case COMMENTS_SET_SORT:
return action.payload;
default:
return state;
}
}
export default combineReducers({
sort,
isFetching,
topComments,
childComments,
allComments,
activeComment,
pinnedComments,
});