reorganize comments reducers
This commit is contained in:
@@ -65,6 +65,14 @@ export interface Comment {
|
||||
delete?: boolean;
|
||||
/** post title */
|
||||
title?: string;
|
||||
/**
|
||||
* @ClientOnly defines whether comments was hidden (deleted)
|
||||
*
|
||||
* Situatuon may occure for example if user decided to hide someone,
|
||||
* in this case we don't use `delete` field because comment with `delete`
|
||||
* still renders, and comment with `hidden` flag completely removed from DOM
|
||||
*/
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export interface CommentsResponse {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { h } from 'preact';
|
||||
import { mount } from 'enzyme';
|
||||
import { Props, Comment } from './comment';
|
||||
import { User, Comment as CommentType, PostInfo } from '@app/common/types';
|
||||
import { delay } from '@app/store/comments/utils';
|
||||
import { sleep } from '@app/utils/sleep';
|
||||
|
||||
const DefaultProps: Partial<Props> = {
|
||||
post_info: {
|
||||
@@ -125,7 +125,7 @@ describe('<Comment />', () => {
|
||||
.getAttribute('aria-disabled')
|
||||
).toStrictEqual('true');
|
||||
voteButtons.at(0).simulate('click');
|
||||
await delay(100);
|
||||
await sleep(100);
|
||||
expect(voteSpy).not.toBeCalled();
|
||||
|
||||
expect(
|
||||
@@ -135,7 +135,7 @@ describe('<Comment />', () => {
|
||||
.getAttribute('aria-disabled')
|
||||
).toStrictEqual('false');
|
||||
voteButtons.at(1).simulate('click');
|
||||
await delay(100);
|
||||
await sleep(100);
|
||||
expect(voteSpy).toBeCalled();
|
||||
}, 30000);
|
||||
|
||||
@@ -159,7 +159,7 @@ describe('<Comment />', () => {
|
||||
.getAttribute('aria-disabled')
|
||||
).toStrictEqual('true');
|
||||
voteButtons.at(1).simulate('click');
|
||||
await delay(100);
|
||||
await sleep(100);
|
||||
expect(voteSpy).not.toBeCalled();
|
||||
|
||||
expect(
|
||||
@@ -169,7 +169,7 @@ describe('<Comment />', () => {
|
||||
.getAttribute('aria-disabled')
|
||||
).toStrictEqual('false');
|
||||
voteButtons.at(0).simulate('click');
|
||||
await delay(100);
|
||||
await sleep(100);
|
||||
expect(voteSpy).toBeCalled();
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
@@ -3,16 +3,7 @@ import { h, Component, RenderableProps } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import {
|
||||
User,
|
||||
Node,
|
||||
PostInfo,
|
||||
BlockedUser,
|
||||
Comment as CommentType,
|
||||
Sorting,
|
||||
Theme,
|
||||
AuthProvider,
|
||||
} from '@app/common/types';
|
||||
import { User, Sorting, AuthProvider } from '@app/common/types';
|
||||
import {
|
||||
NODE_ID,
|
||||
COMMENT_NODE_CLASSNAME_PREFIX,
|
||||
@@ -50,7 +41,21 @@ import { Thread } from '@app/components/thread';
|
||||
import { uploadImage, getPreview } from '@app/common/api';
|
||||
import { isUserAnonymous } from '@app/utils/isUserAnonymous';
|
||||
import { bindActions } from '@app/utils/actionBinder';
|
||||
import { ProviderState } from '@app/store/provider/reducers';
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
user: state.user,
|
||||
sort: state.sort,
|
||||
isSettingsVisible: state.isSettingsVisible,
|
||||
topComments: state.topComments,
|
||||
pinnedComments: state.pinnedComments.map(id => state.comments[id]).filter(c => !c.hidden),
|
||||
provider: state.provider,
|
||||
theme: state.theme,
|
||||
info: state.info,
|
||||
hiddenUsers: state.hiddenUsers,
|
||||
blockedUsers: state.bannedUsers,
|
||||
getPreview,
|
||||
uploadImage,
|
||||
});
|
||||
|
||||
const boundActions = bindActions({
|
||||
fetchComments,
|
||||
@@ -71,20 +76,7 @@ const boundActions = bindActions({
|
||||
updateComment,
|
||||
});
|
||||
|
||||
type Props = {
|
||||
user: User | null;
|
||||
sort: Sorting;
|
||||
comments: Node[];
|
||||
pinnedComments: CommentType[];
|
||||
theme: Theme;
|
||||
info: PostInfo;
|
||||
hiddenUsers: StoreState['hiddenUsers'];
|
||||
blockedUsers: BlockedUser[];
|
||||
isSettingsVisible: boolean;
|
||||
getPreview: typeof getPreview;
|
||||
uploadImage: typeof uploadImage;
|
||||
provider: ProviderState;
|
||||
} & typeof boundActions;
|
||||
type Props = ReturnType<typeof mapStateToProps> & typeof boundActions;
|
||||
|
||||
interface State {
|
||||
isLoaded: boolean;
|
||||
@@ -261,24 +253,34 @@ export class Root extends Component<Props, State> {
|
||||
{this.props.pinnedComments.length > 0 && (
|
||||
<div className="root__pinned-comments" role="region" aria-label="Pinned comments">
|
||||
{this.props.pinnedComments.map(comment => (
|
||||
<Comment view="pinned" data={comment} level={0} disabled={true} mix="root__pinned-comment" />
|
||||
<Comment
|
||||
key={`pinned-comment-${comment.id}`}
|
||||
view="pinned"
|
||||
data={comment}
|
||||
level={0}
|
||||
disabled={true}
|
||||
mix="root__pinned-comment"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!!this.props.comments.length && !isCommentsListLoading && (
|
||||
{!!this.props.topComments.length && !isCommentsListLoading && (
|
||||
<div className="root__threads" role="list">
|
||||
{(IS_MOBILE ? this.props.comments.slice(0, commentsShown) : this.props.comments).map(thread => (
|
||||
{(IS_MOBILE && commentsShown < this.props.topComments.length
|
||||
? this.props.topComments.slice(0, commentsShown)
|
||||
: this.props.topComments
|
||||
).map(id => (
|
||||
<Thread
|
||||
key={thread.comment.id}
|
||||
key={`thread-${id}`}
|
||||
id={id}
|
||||
mix="root__thread"
|
||||
level={0}
|
||||
data={thread}
|
||||
getPreview={this.props.getPreview}
|
||||
/>
|
||||
))}
|
||||
|
||||
{commentsShown < this.props.comments.length && IS_MOBILE && (
|
||||
{commentsShown < this.props.topComments.length && IS_MOBILE && (
|
||||
<button className="root__show-more" onClick={this.showMore}>
|
||||
Show more
|
||||
</button>
|
||||
@@ -323,19 +325,6 @@ export class Root extends Component<Props, State> {
|
||||
|
||||
/** Root component connected to redux */
|
||||
export const ConnectedRoot = connect(
|
||||
(state: StoreState) => ({
|
||||
user: state.user,
|
||||
sort: state.sort,
|
||||
isSettingsVisible: state.isSettingsVisible,
|
||||
comments: state.comments,
|
||||
pinnedComments: state.pinnedComments,
|
||||
theme: state.theme,
|
||||
info: state.info,
|
||||
hiddenUsers: state.hiddenUsers,
|
||||
blockedUsers: state.bannedUsers,
|
||||
provider: state.provider,
|
||||
getPreview,
|
||||
uploadImage,
|
||||
}),
|
||||
mapStateToProps,
|
||||
boundActions
|
||||
)(Root);
|
||||
|
||||
@@ -4,55 +4,54 @@ import { connect } from 'preact-redux';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import { ConnectedComment as Comment } from '@app/components/comment/connected-comment';
|
||||
import { Node, Theme } from '@app/common/types';
|
||||
import { Comment as CommentInterface } from '@app/common/types';
|
||||
import { getThreadIsCollapsed } from '@app/store/thread/getters';
|
||||
import { StoreState } from '@app/store';
|
||||
|
||||
interface Props {
|
||||
collapsed: boolean;
|
||||
data: Node;
|
||||
isCommentsDisabled: boolean;
|
||||
const mapStateToProps = (state: StoreState, props: { id: CommentInterface['id'] }) => {
|
||||
const comment = state.comments[props.id];
|
||||
return {
|
||||
comment,
|
||||
childs: state.childComments[props.id],
|
||||
collapsed: getThreadIsCollapsed(state, comment),
|
||||
isCommentsDisabled: !!state.info.read_only,
|
||||
theme: state.theme,
|
||||
};
|
||||
};
|
||||
|
||||
type Props = {
|
||||
id: CommentInterface['id'];
|
||||
childs?: (CommentInterface['id'])[];
|
||||
level: number;
|
||||
theme: Theme;
|
||||
mix?: string;
|
||||
|
||||
getPreview(text: string): Promise<string>;
|
||||
}
|
||||
} & ReturnType<typeof mapStateToProps>;
|
||||
|
||||
function Thread(props: RenderableProps<Props>) {
|
||||
const {
|
||||
collapsed,
|
||||
data: { comment, replies = [] },
|
||||
level,
|
||||
theme,
|
||||
} = props;
|
||||
const { collapsed, comment, childs, level, theme } = props;
|
||||
|
||||
if (comment.hidden) return null;
|
||||
|
||||
const indented = level > 0;
|
||||
const repliesCount = childs ? childs.length : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={b('thread', props, { level, theme, indented })}
|
||||
role={['listitem'].concat(!collapsed && replies.length ? 'list' : []).join(' ')}
|
||||
role={['listitem'].concat(!collapsed && !!repliesCount ? 'list' : []).join(' ')}
|
||||
aria-expanded={!collapsed}
|
||||
>
|
||||
<Comment view="main" data={comment} repliesCount={replies.length} level={level} />
|
||||
<Comment key={`comment-${props.id}`} view="main" data={comment} repliesCount={repliesCount} level={level} />
|
||||
|
||||
{!collapsed &&
|
||||
!!replies.length &&
|
||||
replies.map(thread => (
|
||||
<ConnectedThread
|
||||
key={thread.comment.id}
|
||||
data={thread}
|
||||
level={Math.min(level + 1, 6)}
|
||||
getPreview={props.getPreview}
|
||||
/>
|
||||
childs &&
|
||||
!!childs.length &&
|
||||
childs.map(id => (
|
||||
<ConnectedThread key={`thread-${id}`} id={id} level={Math.min(level + 1, 6)} getPreview={props.getPreview} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const ConnectedThread = connect((state: StoreState, props: { data: Node }) => ({
|
||||
collapsed: getThreadIsCollapsed(state, props.data.comment),
|
||||
isCommentsDisabled: !!state.info.read_only,
|
||||
theme: state.theme,
|
||||
}))(Thread);
|
||||
export const ConnectedThread = connect(mapStateToProps)(Thread);
|
||||
|
||||
@@ -1,56 +1,40 @@
|
||||
import api from '@app/common/api';
|
||||
import { Tree, Comment, Sorting, CommentMode } from '@app/common/types';
|
||||
import { Tree, Comment, Sorting, CommentMode, Node } from '@app/common/types';
|
||||
|
||||
import { StoreAction, StoreState } from '../index';
|
||||
import { POST_INFO_SET } from '../post_info/types';
|
||||
import {
|
||||
getPinnedComments,
|
||||
addComment as uAddComment,
|
||||
replaceComment as uReplaceComment,
|
||||
removeComment as uRemoveComment,
|
||||
setCommentPin as uSetCommentPin,
|
||||
filterTree,
|
||||
} from './utils';
|
||||
import { COMMENTS_SET, PINNED_COMMENTS_SET, COMMENT_MODE_SET } from './types';
|
||||
import { filterTree } from './utils';
|
||||
import { COMMENTS_SET, COMMENT_MODE_SET, COMMENTS_APPEND, COMMENTS_EDIT } from './types';
|
||||
|
||||
/** sets comments, and put pinned comments in cache */
|
||||
export const setComments = (comments: StoreState['comments']): StoreAction<void> => dispatch => {
|
||||
export const setComments = (comments: Node[]): StoreAction<void> => dispatch => {
|
||||
dispatch({
|
||||
type: COMMENTS_SET,
|
||||
comments,
|
||||
});
|
||||
dispatch({
|
||||
type: PINNED_COMMENTS_SET,
|
||||
comments: getPinnedComments(comments),
|
||||
});
|
||||
};
|
||||
|
||||
/** appends comment to tree */
|
||||
export const addComment = (text: string, title: string, pid?: Comment['id']): StoreAction<Promise<void>> => async (
|
||||
dispatch,
|
||||
getState
|
||||
) => {
|
||||
export const addComment = (
|
||||
text: string,
|
||||
title: string,
|
||||
pid?: Comment['id']
|
||||
): StoreAction<Promise<void>> => async dispatch => {
|
||||
const comment = await api.addComment({ text, title, pid });
|
||||
const comments = getState().comments;
|
||||
dispatch(setComments(uAddComment(comments, comment)));
|
||||
dispatch({ type: COMMENTS_APPEND, pid: pid || null, comment });
|
||||
};
|
||||
|
||||
/** edits comment in tree */
|
||||
export const updateComment = (id: Comment['id'], text: string): StoreAction<Promise<void>> => async (
|
||||
dispatch,
|
||||
getState
|
||||
) => {
|
||||
export const updateComment = (id: Comment['id'], text: string): StoreAction<Promise<void>> => async dispatch => {
|
||||
const comment = await api.updateComment({ id, text });
|
||||
const comments = getState().comments;
|
||||
dispatch(setComments(uReplaceComment(comments, comment)));
|
||||
dispatch({ type: COMMENTS_EDIT, comment });
|
||||
};
|
||||
|
||||
/** edits comment in tree */
|
||||
export const putVote = (id: Comment['id'], value: number): StoreAction<Promise<void>> => async (dispatch, getState) => {
|
||||
export const putVote = (id: Comment['id'], value: number): StoreAction<Promise<void>> => async dispatch => {
|
||||
await api.putCommentVote({ id, value });
|
||||
const updatedComment = await api.getComment(id);
|
||||
const comments = getState().comments;
|
||||
dispatch(setComments(uReplaceComment(comments, updatedComment)));
|
||||
const comment = await api.getComment(id);
|
||||
dispatch({ type: COMMENTS_EDIT, comment });
|
||||
};
|
||||
|
||||
/** edits comment in tree */
|
||||
@@ -63,8 +47,9 @@ export const setPinState = (id: Comment['id'], value: boolean): StoreAction<Prom
|
||||
} else {
|
||||
await api.unpinComment(id);
|
||||
}
|
||||
const comments = getState().comments;
|
||||
dispatch(setComments(uSetCommentPin(comments, id, value)));
|
||||
let comment = getState().comments[id];
|
||||
comment = { ...comment, pin: value, edit: { summary: '', time: new Date().toISOString() } };
|
||||
dispatch({ type: COMMENTS_EDIT, comment });
|
||||
};
|
||||
|
||||
/** edits comment in tree */
|
||||
@@ -76,8 +61,9 @@ export const removeComment = (id: Comment['id']): StoreAction<Promise<void>> =>
|
||||
} else {
|
||||
await api.removeMyComment(id);
|
||||
}
|
||||
const comments = getState().comments;
|
||||
dispatch(setComments(uRemoveComment(comments, id)));
|
||||
let comment = getState().comments[id];
|
||||
comment = { ...comment, delete: true, edit: { summary: '', time: new Date().toISOString() } };
|
||||
dispatch({ type: COMMENTS_EDIT, comment });
|
||||
};
|
||||
|
||||
/** fetches comments from server */
|
||||
|
||||
@@ -3,16 +3,115 @@ import { Node, Comment, CommentMode } from '@app/common/types';
|
||||
import {
|
||||
COMMENTS_SET,
|
||||
COMMENTS_SET_ACTION,
|
||||
PINNED_COMMENTS_SET_ACTION,
|
||||
PINNED_COMMENTS_SET,
|
||||
COMMENT_MODE_SET,
|
||||
COMMENT_MODE_SET_ACTION,
|
||||
COMMENTS_APPEND_ACTION,
|
||||
COMMENTS_APPEND,
|
||||
COMMENTS_EDIT_ACTION,
|
||||
COMMENTS_EDIT,
|
||||
COMMENTS_PATCH,
|
||||
COMMENTS_PATCH_ACTION,
|
||||
} from './types';
|
||||
import { getPinnedComments } from './utils';
|
||||
import { cmpRef } from '@app/utils/cmpRef';
|
||||
|
||||
export const comments = (state: Node[] = [], action: COMMENTS_SET_ACTION): Node[] => {
|
||||
export const topComments = (
|
||||
state: (Comment['id'])[] = [],
|
||||
action: COMMENTS_SET_ACTION | COMMENTS_APPEND_ACTION
|
||||
): (Comment['id'])[] => {
|
||||
switch (action.type) {
|
||||
case COMMENTS_SET: {
|
||||
return action.comments;
|
||||
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 comments = (
|
||||
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 (!state.hasOwnProperty(id)) continue;
|
||||
if (!changed) {
|
||||
changed = true;
|
||||
newState = { ...newState };
|
||||
}
|
||||
newState[id] = { ...newState[id], edit: editObject, ...action.patch };
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
@@ -34,14 +133,41 @@ export const activeComment = (
|
||||
}
|
||||
};
|
||||
|
||||
export const pinnedComments = (state: Comment[] = [], action: PINNED_COMMENTS_SET_ACTION): Comment[] => {
|
||||
export const pinnedComments = (
|
||||
state: (Comment['id'])[] = [],
|
||||
action: COMMENTS_SET_ACTION | COMMENTS_EDIT_ACTION | COMMENTS_PATCH_ACTION
|
||||
): (Comment['id'])[] => {
|
||||
switch (action.type) {
|
||||
case PINNED_COMMENTS_SET: {
|
||||
return action.comments;
|
||||
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 (!action.patch.hasOwnProperty('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;
|
||||
}
|
||||
};
|
||||
|
||||
export default { comments, activeComment, pinnedComments };
|
||||
export default { topComments, childComments, comments, activeComment, pinnedComments };
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
import { Node } from '@app/common/types';
|
||||
import { Node, Comment } from '@app/common/types';
|
||||
import { StoreState } from '../index';
|
||||
|
||||
export const COMMENTS_SET = 'COMMENTS/SET';
|
||||
|
||||
export interface COMMENTS_SET_ACTION {
|
||||
type: typeof COMMENTS_SET;
|
||||
comments: StoreState['comments'];
|
||||
comments: Node[];
|
||||
}
|
||||
|
||||
export const COMMENTS_APPEND = 'COMMENTS/APPEND';
|
||||
|
||||
export interface COMMENTS_APPEND_ACTION {
|
||||
type: typeof COMMENTS_APPEND;
|
||||
comments: Node;
|
||||
comment: Comment;
|
||||
}
|
||||
|
||||
export const PINNED_COMMENTS_SET = 'PINNED_COMMENTS/SET';
|
||||
export const COMMENTS_EDIT = 'COMMENTS/EDIT';
|
||||
|
||||
export interface PINNED_COMMENTS_SET_ACTION {
|
||||
type: typeof PINNED_COMMENTS_SET;
|
||||
comments: StoreState['pinnedComments'];
|
||||
export interface COMMENTS_EDIT_ACTION {
|
||||
type: typeof COMMENTS_EDIT;
|
||||
comment: Comment;
|
||||
}
|
||||
|
||||
export const COMMENTS_PATCH = 'COMMENTS/PATCH';
|
||||
|
||||
export interface COMMENTS_PATCH_ACTION {
|
||||
type: typeof COMMENTS_PATCH;
|
||||
ids: (Comment['id'])[];
|
||||
patch: Partial<Comment>;
|
||||
}
|
||||
|
||||
export const COMMENT_MODE_SET = 'COMMENT_MODE/SET';
|
||||
@@ -32,5 +40,6 @@ export interface COMMENT_MODE_SET_ACTION {
|
||||
export type COMMENTS_ACTIONS =
|
||||
| COMMENTS_SET_ACTION
|
||||
| COMMENTS_APPEND_ACTION
|
||||
| PINNED_COMMENTS_SET_ACTION
|
||||
| COMMENTS_EDIT_ACTION
|
||||
| COMMENTS_PATCH_ACTION
|
||||
| COMMENT_MODE_SET_ACTION;
|
||||
|
||||
@@ -1,41 +1,4 @@
|
||||
import { Comment, Node, User } from '@app/common/types';
|
||||
|
||||
/**
|
||||
* Traverses through tree and applies function to comment with given id.
|
||||
* Note that function must not mutate comment, or rerender will not happen
|
||||
*/
|
||||
function mapTreeIfID(tree: Node[], id: Comment['id'], fn: (c: Node) => Node): Node[] {
|
||||
// path of indexes to comment with given id
|
||||
let path: number[] = [];
|
||||
const subfn = (tree: Node[], level: number): boolean => {
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
path = path.slice(0, level);
|
||||
path.push(i);
|
||||
if (id === tree[i].comment.id) return true;
|
||||
if (tree[i].replies) {
|
||||
if (subfn(tree[i].replies!, level + 1)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (!subfn(tree, 0)) return tree;
|
||||
|
||||
// dereferencing (cloning) node path to comment with id,
|
||||
// so react will cause rerender
|
||||
const treeClone = [...tree];
|
||||
let subtree = treeClone;
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const index = path[i];
|
||||
if (i === path.length - 1) {
|
||||
subtree[index] = fn(subtree[index]);
|
||||
break;
|
||||
}
|
||||
subtree[index] = { comment: subtree[index].comment, replies: [...subtree[index].replies!] };
|
||||
subtree = subtree[index].replies!;
|
||||
}
|
||||
|
||||
return treeClone;
|
||||
}
|
||||
import { Comment, Node } from '@app/common/types';
|
||||
|
||||
/**
|
||||
* Filters tree node
|
||||
@@ -58,22 +21,6 @@ export function filterTree(tree: Node[], fn: (node: Node) => boolean): Node[] {
|
||||
return newTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses through tree and applies function to comment on which function passed.
|
||||
* Note that function must not mutate comment
|
||||
*/
|
||||
export function mapTree(tree: Node[], fn: (c: Comment) => Comment): Node[] {
|
||||
return tree.map(node => {
|
||||
const clone: Node = {
|
||||
comment: fn(node.comment),
|
||||
};
|
||||
if (node.replies) {
|
||||
clone.replies = mapTree(node.replies, fn);
|
||||
}
|
||||
return clone;
|
||||
});
|
||||
}
|
||||
|
||||
export function findPinnedComments(thread: Node): Comment[] {
|
||||
let result: Comment[] = [];
|
||||
|
||||
@@ -93,74 +40,3 @@ export function findPinnedComments(thread: Node): Comment[] {
|
||||
export function getPinnedComments(threads: Node[]): Comment[] {
|
||||
return threads.reduce((acc: Comment[], thread: Node) => acc.concat(findPinnedComments(thread)), []);
|
||||
}
|
||||
|
||||
export function removeComment(comments: Node[], id: Comment['id']): Node[] {
|
||||
return mapTreeIfID(
|
||||
comments,
|
||||
id,
|
||||
(n): Node => ({
|
||||
comment: {
|
||||
...n.comment,
|
||||
delete: true,
|
||||
},
|
||||
replies: n.replies,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function setCommentPin(comments: Node[], id: Comment['id'], value: boolean): Node[] {
|
||||
return mapTreeIfID(
|
||||
comments,
|
||||
id,
|
||||
(n): Node => ({
|
||||
comment: {
|
||||
...n.comment,
|
||||
pin: value,
|
||||
},
|
||||
replies: n.replies,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function setUserVerified(comments: Node[], userId: User['id'], value: boolean): Node[] {
|
||||
return mapTree(comments, comment => {
|
||||
if (comment.user.id !== userId) return comment;
|
||||
return {
|
||||
...comment,
|
||||
user: {
|
||||
...comment.user,
|
||||
verified: value,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function pasteReply(comments: Node[], reply: Comment): Node[] {
|
||||
return mapTreeIfID(
|
||||
comments,
|
||||
reply.pid,
|
||||
(n): Node => {
|
||||
const nn = { ...n };
|
||||
if (!nn.replies) nn.replies = [];
|
||||
nn.replies = [{ comment: reply }, ...nn.replies];
|
||||
return nn;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function addComment(comments: Node[], comment: Comment): Node[] {
|
||||
if (comment.pid !== '') {
|
||||
return pasteReply(comments, comment);
|
||||
}
|
||||
return [{ comment }, ...comments];
|
||||
}
|
||||
|
||||
export function replaceComment(comments: Node[], comment: Comment): Node[] {
|
||||
return mapTreeIfID(comments, comment.id, n => ({ ...n, comment }));
|
||||
}
|
||||
|
||||
export function delay(ms: number = 100): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PostInfo } from '@app/common/types';
|
||||
import { POST_INFO_SET, POST_INFO_SET_ACTION } from './types';
|
||||
import { cmpRef } from '@app/utils/cmpRef';
|
||||
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
const DefaultPostInfo: PostInfo = {
|
||||
@@ -14,7 +15,7 @@ const DefaultPostInfo: PostInfo = {
|
||||
export const info = (state: PostInfo = DefaultPostInfo, action: POST_INFO_SET_ACTION): PostInfo => {
|
||||
switch (action.type) {
|
||||
case POST_INFO_SET: {
|
||||
return action.info;
|
||||
return cmpRef(state, action.info);
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
|
||||
@@ -8,20 +8,16 @@ import {
|
||||
USER_SET,
|
||||
USER_UNBAN,
|
||||
USER_BANLIST_SET,
|
||||
USER_HIDELIST_SET_ACTION,
|
||||
USER_HIDELIST_SET,
|
||||
USER_HIDE_ACTION,
|
||||
USER_HIDE,
|
||||
USER_UNHIDE_ACTION,
|
||||
USER_UNHIDE,
|
||||
SETTINGS_VISIBLE_SET,
|
||||
} from './types';
|
||||
import { setComments, unsetCommentMode } from '../comments/actions';
|
||||
import { setUserVerified as uSetUserVerified, filterTree, mapTree } from '../comments/utils';
|
||||
import { unsetCommentMode } from '../comments/actions';
|
||||
import { IS_STORAGE_AVAILABLE, LS_HIDDEN_USERS_KEY } from '@app/common/constants';
|
||||
import { getItem } from '@app/common/local-storage';
|
||||
import { Dispatch } from 'redux';
|
||||
import { updateProvider } from '../provider/actions';
|
||||
import { COMMENTS_PATCH } from '../comments/types';
|
||||
|
||||
export const fetchUser = (): StoreAction<Promise<User | null>> => async dispatch => {
|
||||
const user = await api.getUser();
|
||||
@@ -78,23 +74,27 @@ export const blockUser = (
|
||||
|
||||
export const unblockUser = (id: User['id']): StoreAction<Promise<void>> => async (dispatch, getState) => {
|
||||
await api.unblockUser(id);
|
||||
const comments = mapTree(getState().comments, c => {
|
||||
if (c.user.id !== id) return c;
|
||||
if (!c.user.block) return c;
|
||||
return { ...c, user: { ...c.user, block: false } };
|
||||
});
|
||||
dispatch({
|
||||
type: USER_UNBAN,
|
||||
id,
|
||||
});
|
||||
dispatch(setComments(comments));
|
||||
const comments = Object.values(getState().comments).filter(c => c.user.id === id);
|
||||
|
||||
if (!comments.length) return;
|
||||
const user = comments[0].user;
|
||||
|
||||
dispatch({
|
||||
type: COMMENTS_PATCH,
|
||||
ids: comments.map(c => c.id),
|
||||
patch: { user: { ...user, block: false } },
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchHiddenUsers = (): StoreAction<void> => dispatch => {
|
||||
if (!IS_STORAGE_AVAILABLE) return;
|
||||
|
||||
const hiddenUsers = JSON.parse(getItem(LS_HIDDEN_USERS_KEY) || '{}');
|
||||
return (dispatch as Dispatch<USER_HIDELIST_SET_ACTION>)({ type: USER_HIDELIST_SET, payload: hiddenUsers });
|
||||
dispatch({ type: USER_HIDELIST_SET, payload: hiddenUsers });
|
||||
};
|
||||
|
||||
export const hideUser = (user: User): StoreAction<void> => (dispatch, getState) => {
|
||||
@@ -103,13 +103,18 @@ export const hideUser = (user: User): StoreAction<void> => (dispatch, getState)
|
||||
hiddenUsers[user.id] = user;
|
||||
localStorage.setItem(LS_HIDDEN_USERS_KEY, JSON.stringify(hiddenUsers));
|
||||
}
|
||||
(dispatch as Dispatch<USER_HIDE_ACTION>)({ type: USER_HIDE, user });
|
||||
dispatch({ type: USER_HIDE, user });
|
||||
|
||||
const comments = getState().comments;
|
||||
return dispatch(setComments(filterTree(comments, node => node.comment.user.id !== user.id)));
|
||||
dispatch({
|
||||
type: COMMENTS_PATCH,
|
||||
ids: Object.values(getState().comments)
|
||||
.filter(c => c.user.id === user.id)
|
||||
.map(c => c.id),
|
||||
patch: { hidden: true },
|
||||
});
|
||||
};
|
||||
|
||||
export const unhideUser = (userId: string): StoreAction<void> => dispatch => {
|
||||
export const unhideUser = (userId: string): StoreAction<void> => (dispatch, getState) => {
|
||||
if (IS_STORAGE_AVAILABLE) {
|
||||
const hiddenUsers = JSON.parse(getItem(LS_HIDDEN_USERS_KEY) || '{}');
|
||||
if (hiddenUsers.hasOwnProperty(userId)) {
|
||||
@@ -117,7 +122,15 @@ export const unhideUser = (userId: string): StoreAction<void> => dispatch => {
|
||||
}
|
||||
localStorage.setItem(LS_HIDDEN_USERS_KEY, JSON.stringify(hiddenUsers));
|
||||
}
|
||||
return (dispatch as Dispatch<USER_UNHIDE_ACTION>)({ type: USER_UNHIDE, id: userId });
|
||||
|
||||
dispatch({ type: USER_UNHIDE, id: userId });
|
||||
dispatch({
|
||||
type: COMMENTS_PATCH,
|
||||
ids: Object.values(getState().comments)
|
||||
.filter(c => c.user.id === userId)
|
||||
.map(c => c.id),
|
||||
patch: { hidden: false },
|
||||
});
|
||||
};
|
||||
|
||||
export const setVerifiedStatus = (id: User['id'], status: boolean): StoreAction<Promise<void>> => async (
|
||||
@@ -129,8 +142,15 @@ export const setVerifiedStatus = (id: User['id'], status: boolean): StoreAction<
|
||||
} else {
|
||||
await api.removeVerifyStatus(id);
|
||||
}
|
||||
const comments = getState().comments;
|
||||
dispatch(setComments(uSetUserVerified(comments, id, status)));
|
||||
const comments = Object.values(getState().comments).filter(c => c.user.id === id);
|
||||
if (!comments.length) return;
|
||||
const user = comments[0].user;
|
||||
|
||||
dispatch({
|
||||
type: COMMENTS_PATCH,
|
||||
ids: comments.map(c => c.id),
|
||||
patch: { user: { ...user, verified: status } },
|
||||
});
|
||||
};
|
||||
|
||||
export const setSettingsVisibleState = (state: boolean): StoreAction<boolean> => dispatch => {
|
||||
|
||||
Reference in New Issue
Block a user