reorganize comments reducers

This commit is contained in:
Vyrtsev Mikhail
2019-07-29 13:47:35 -05:00
committed by Umputun
parent 48de9e15a3
commit 8d748b6c65
10 changed files with 289 additions and 275 deletions
+8
View File
@@ -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);
});
+34 -45
View File
@@ -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);
+27 -28
View File
@@ -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);
+21 -35
View File
@@ -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 */
+134 -8
View File
@@ -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 };
+17 -8
View File
@@ -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 -125
View File
@@ -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);
});
}
+2 -1
View File
@@ -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;
+40 -20
View File
@@ -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 => {