From 8d748b6c65186607ea5a5c64212cfff6769e9883 Mon Sep 17 00:00:00 2001 From: Vyrtsev Mikhail Date: Sun, 7 Jul 2019 05:49:47 +0300 Subject: [PATCH] reorganize comments reducers --- frontend/app/common/types.ts | 8 + .../app/components/comment/comment.test.tsx | 10 +- frontend/app/components/root/root.tsx | 79 +++++----- frontend/app/components/thread/thread.tsx | 55 ++++--- frontend/app/store/comments/actions.ts | 56 +++---- frontend/app/store/comments/reducers.ts | 142 +++++++++++++++++- frontend/app/store/comments/types.ts | 25 ++- frontend/app/store/comments/utils.ts | 126 +--------------- frontend/app/store/post_info/reducers.ts | 3 +- frontend/app/store/user/actions.ts | 60 +++++--- 10 files changed, 289 insertions(+), 275 deletions(-) diff --git a/frontend/app/common/types.ts b/frontend/app/common/types.ts index 8f69aaa4..debc98d5 100644 --- a/frontend/app/common/types.ts +++ b/frontend/app/common/types.ts @@ -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 { diff --git a/frontend/app/components/comment/comment.test.tsx b/frontend/app/components/comment/comment.test.tsx index 50560a4a..7f86b714 100644 --- a/frontend/app/components/comment/comment.test.tsx +++ b/frontend/app/components/comment/comment.test.tsx @@ -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 = { post_info: { @@ -125,7 +125,7 @@ describe('', () => { .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('', () => { .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('', () => { .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('', () => { .getAttribute('aria-disabled') ).toStrictEqual('false'); voteButtons.at(0).simulate('click'); - await delay(100); + await sleep(100); expect(voteSpy).toBeCalled(); }, 30000); }); diff --git a/frontend/app/components/root/root.tsx b/frontend/app/components/root/root.tsx index 84845fa4..7744be91 100644 --- a/frontend/app/components/root/root.tsx +++ b/frontend/app/components/root/root.tsx @@ -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 boundActions; interface State { isLoaded: boolean; @@ -261,24 +253,34 @@ export class Root extends Component { {this.props.pinnedComments.length > 0 && (
{this.props.pinnedComments.map(comment => ( - + ))}
)} - {!!this.props.comments.length && !isCommentsListLoading && ( + {!!this.props.topComments.length && !isCommentsListLoading && (
- {(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 => ( ))} - {commentsShown < this.props.comments.length && IS_MOBILE && ( + {commentsShown < this.props.topComments.length && IS_MOBILE && ( @@ -323,19 +325,6 @@ export class Root extends Component { /** 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); diff --git a/frontend/app/components/thread/thread.tsx b/frontend/app/components/thread/thread.tsx index d51ebf25..9ce98017 100644 --- a/frontend/app/components/thread/thread.tsx +++ b/frontend/app/components/thread/thread.tsx @@ -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; -} +} & ReturnType; function Thread(props: RenderableProps) { - 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 (
- + {!collapsed && - !!replies.length && - replies.map(thread => ( - + childs && + !!childs.length && + childs.map(id => ( + ))}
); } -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); diff --git a/frontend/app/store/comments/actions.ts b/frontend/app/store/comments/actions.ts index 5fbeb4bd..09d853de 100644 --- a/frontend/app/store/comments/actions.ts +++ b/frontend/app/store/comments/actions.ts @@ -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 => dispatch => { +export const setComments = (comments: Node[]): StoreAction => 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> => async ( - dispatch, - getState -) => { +export const addComment = ( + text: string, + title: string, + pid?: Comment['id'] +): StoreAction> => 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> => async ( - dispatch, - getState -) => { +export const updateComment = (id: Comment['id'], text: string): StoreAction> => 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> => async (dispatch, getState) => { +export const putVote = (id: Comment['id'], value: number): StoreAction> => 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> => } 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 */ diff --git a/frontend/app/store/comments/reducers.ts b/frontend/app/store/comments/reducers.ts index be632636..8ece8c96 100644 --- a/frontend/app/store/comments/reducers.ts +++ b/frontend/app/store/comments/reducers.ts @@ -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, + x: Node +): Record => { + 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 = {}, + action: COMMENTS_SET_ACTION | COMMENTS_APPEND_ACTION +): Record => { + switch (action.type) { + case COMMENTS_SET: { + return action.comments.reduce>(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, x: Node): Record => { + 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 = {}, + action: COMMENTS_SET_ACTION | COMMENTS_APPEND_ACTION | COMMENTS_EDIT_ACTION | COMMENTS_PATCH_ACTION +): Record => { + switch (action.type) { + case COMMENTS_SET: { + return action.comments.reduce>(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 }; diff --git a/frontend/app/store/comments/types.ts b/frontend/app/store/comments/types.ts index 3b82479c..c2bc79aa 100644 --- a/frontend/app/store/comments/types.ts +++ b/frontend/app/store/comments/types.ts @@ -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; } 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; diff --git a/frontend/app/store/comments/utils.ts b/frontend/app/store/comments/utils.ts index 590f04b1..c7a17393 100644 --- a/frontend/app/store/comments/utils.ts +++ b/frontend/app/store/comments/utils.ts @@ -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 { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); -} diff --git a/frontend/app/store/post_info/reducers.ts b/frontend/app/store/post_info/reducers.ts index 72f3befc..337782bf 100644 --- a/frontend/app/store/post_info/reducers.ts +++ b/frontend/app/store/post_info/reducers.ts @@ -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; diff --git a/frontend/app/store/user/actions.ts b/frontend/app/store/user/actions.ts index e0906fe1..8c9ebde3 100644 --- a/frontend/app/store/user/actions.ts +++ b/frontend/app/store/user/actions.ts @@ -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> => async dispatch => { const user = await api.getUser(); @@ -78,23 +74,27 @@ export const blockUser = ( export const unblockUser = (id: User['id']): StoreAction> => 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 => dispatch => { if (!IS_STORAGE_AVAILABLE) return; const hiddenUsers = JSON.parse(getItem(LS_HIDDEN_USERS_KEY) || '{}'); - return (dispatch as Dispatch)({ type: USER_HIDELIST_SET, payload: hiddenUsers }); + dispatch({ type: USER_HIDELIST_SET, payload: hiddenUsers }); }; export const hideUser = (user: User): StoreAction => (dispatch, getState) => { @@ -103,13 +103,18 @@ export const hideUser = (user: User): StoreAction => (dispatch, getState) hiddenUsers[user.id] = user; localStorage.setItem(LS_HIDDEN_USERS_KEY, JSON.stringify(hiddenUsers)); } - (dispatch as Dispatch)({ 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 => dispatch => { +export const unhideUser = (userId: string): StoreAction => (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 => dispatch => { } localStorage.setItem(LS_HIDDEN_USERS_KEY, JSON.stringify(hiddenUsers)); } - return (dispatch as Dispatch)({ 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> => 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 => dispatch => {