diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js index bef306e31..4a3db6a8d 100644 --- a/flow-typed/Comment.js +++ b/flow-typed/Comment.js @@ -25,6 +25,7 @@ declare type CommentsState = { isFetchingReacts: boolean, myReactsByCommentId: any, othersReactsByCommentId: any, + typesReacting: Array, }; declare type CommentReactParams = { diff --git a/static/app-strings.json b/static/app-strings.json index 7deb6252d..4ac6b995c 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -1297,5 +1297,6 @@ "Credit Details": "Credit Details", "Sign In": "Sign In", "Change to tile layout": "Change to tile layout", + "%total_comments% comments": "%total_comments% comments", "--end--": "--end--" } diff --git a/ui/component/commentReactions/index.js b/ui/component/commentReactions/index.js index 9dc76dafe..02a845761 100644 --- a/ui/component/commentReactions/index.js +++ b/ui/component/commentReactions/index.js @@ -1,11 +1,16 @@ import { connect } from 'react-redux'; import Comment from './view'; -import { makeSelectMyReactionsForComment, makeSelectOthersReactionsForComment } from 'redux/selectors/comments'; +import { + makeSelectMyReactionsForComment, + makeSelectOthersReactionsForComment, + selectTypesReacting, +} from 'redux/selectors/comments'; import { doCommentReact } from 'redux/actions/comments'; const select = (state, props) => ({ myReacts: makeSelectMyReactionsForComment(props.commentId)(state), othersReacts: makeSelectOthersReactionsForComment(props.commentId)(state), + typesReacting: selectTypesReacting(state), }); const perform = dispatch => ({ diff --git a/ui/component/commentReactions/view.jsx b/ui/component/commentReactions/view.jsx index d23d70981..cb043db7d 100644 --- a/ui/component/commentReactions/view.jsx +++ b/ui/component/commentReactions/view.jsx @@ -11,10 +11,11 @@ type Props = { othersReacts: any, react: (string, string) => void, commentId: string, + typesReacting: Array, }; export default function CommentReactions(props: Props) { - const { myReacts, othersReacts, commentId, react } = props; + const { myReacts, othersReacts, commentId, react, typesReacting } = props; const [activeChannel] = usePersistedState('comment-channel'); const getCountForReact = type => { @@ -36,7 +37,7 @@ export default function CommentReactions(props: Props) { className={classnames('comment__action', { 'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.LIKE), })} - disabled={!activeChannel} + disabled={!activeChannel || typesReacting.includes(REACTION_TYPES.LIKE)} onClick={() => react(commentId, REACTION_TYPES.LIKE)} label={getCountForReact(REACTION_TYPES.LIKE)} /> @@ -46,6 +47,7 @@ export default function CommentReactions(props: Props) { className={classnames('comment__action', { 'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.DISLIKE), })} + disabled={!activeChannel || typesReacting.includes(REACTION_TYPES.DISLIKE)} onClick={() => react(commentId, REACTION_TYPES.DISLIKE)} label={getCountForReact(REACTION_TYPES.DISLIKE)} /> diff --git a/ui/redux/actions/comments.js b/ui/redux/actions/comments.js index 490e29860..604e3986d 100644 --- a/ui/redux/actions/comments.js +++ b/ui/redux/actions/comments.js @@ -3,7 +3,11 @@ import * as ACTIONS from 'constants/action_types'; import * as REACTION_TYPES from 'constants/reactions'; import { Lbry, selectClaimsByUri, selectMyChannelClaims } from 'lbry-redux'; import { doToast } from 'redux/actions/notifications'; -import { makeSelectCommentIdsForUri, makeSelectMyReactionsForComment } from 'redux/selectors/comments'; +import { + makeSelectCommentIdsForUri, + makeSelectMyReactionsForComment, + makeSelectOthersReactionsForComment, +} from 'redux/selectors/comments'; export function doCommentList(uri: string, page: number = 1, pageSize: number = 99999) { return (dispatch: Dispatch, getState: GetState) => { @@ -45,7 +49,6 @@ export function doCommentReactList(uri: string | null, commentId?: string) { return (dispatch: Dispatch, getState: GetState) => { const state = getState(); const channel = localStorage.getItem('comment-channel'); - // if not channel, fail? if (!channel) { dispatch({ type: ACTIONS.COMMENT_REACTION_LIST_FAILED, @@ -88,7 +91,6 @@ export function doCommentReact(commentId: string, type: string) { return (dispatch: Dispatch, getState: GetState) => { const state = getState(); const channel = localStorage.getItem('comment-channel'); - // if not channel, fail? if (!channel) { dispatch({ type: ACTIONS.COMMENT_REACTION_LIST_FAILED, @@ -97,7 +99,9 @@ export function doCommentReact(commentId: string, type: string) { return; } const myChannels = selectMyChannelClaims(state); - const myReacts = makeSelectMyReactionsForComment(commentId)(state); + let myReacts = makeSelectMyReactionsForComment(commentId)(state); + let reactingTypes = []; + const othersReacts = makeSelectOthersReactionsForComment(commentId)(state); const claimForChannelName = myChannels.find(chan => chan.name === channel); const channelId = claimForChannelName && claimForChannelName.claim_id; const exclusiveTypes = { @@ -105,33 +109,55 @@ export function doCommentReact(commentId: string, type: string) { [REACTION_TYPES.DISLIKE]: REACTION_TYPES.LIKE, }; - dispatch({ - type: ACTIONS.COMMENT_REACT_STARTED, - }); const params: CommentReactParams = { comment_ids: commentId, channel_name: channel, channel_id: channelId, react_type: type, }; - if (Object.keys(exclusiveTypes).includes(type)) { - params['clear_types'] = exclusiveTypes[type]; - } if (myReacts.includes(type)) { params['remove'] = true; + myReacts.splice(myReacts.indexOf(type), 1); + reactingTypes.push(type); + } else { + myReacts.push(type); + reactingTypes.push(type); + if (Object.keys(exclusiveTypes).includes(type)) { + params['clear_types'] = exclusiveTypes[type]; + reactingTypes.push(exclusiveTypes[type]); + if (myReacts.indexOf(exclusiveTypes[type]) !== -1) { + myReacts.splice(myReacts.indexOf(exclusiveTypes[type]), 1); + } + } } + dispatch({ + type: ACTIONS.COMMENT_REACT_STARTED, + data: reactingTypes, + }); + // simulate api return shape: ['like'] -> { 'like': 1 } + const myReactsObj = myReacts.reduce((acc, el) => { + acc[el] = 1; + return acc; + }, {}); Lbry.comment_react(params) .then((result: CommentReactListResponse) => { dispatch({ type: ACTIONS.COMMENT_REACT_COMPLETED, + data: reactingTypes, + }); + dispatch({ + type: ACTIONS.COMMENT_REACTION_LIST_COMPLETED, + data: { + myReactions: { [commentId]: myReactsObj }, + othersReactions: { [commentId]: othersReacts }, + }, }); - dispatch(doCommentReactList(null, commentId)); }) .catch(error => { dispatch({ type: ACTIONS.COMMENT_REACT_FAILED, - data: error, + data: reactingTypes, }); }); }; diff --git a/ui/redux/reducers/comments.js b/ui/redux/reducers/comments.js index f3a6d5908..b19101b73 100644 --- a/ui/redux/reducers/comments.js +++ b/ui/redux/reducers/comments.js @@ -12,6 +12,7 @@ const defaultState: CommentsState = { isCommenting: false, myComments: undefined, isFetchingReacts: false, + typesReacting: [], myReactsByCommentId: {}, othersReactsByCommentId: {}, }; @@ -81,23 +82,56 @@ export default handleActions( isFetchingReacts: false, }), + [ACTIONS.COMMENT_REACT_FAILED]: (state: CommentsState, action: any): CommentsState => { + return { + ...state, + typesReacting: [], + }; + }, + + [ACTIONS.COMMENT_REACT_STARTED]: (state: CommentsState, action: any): CommentsState => { + const reactingTypes = action.data; + const newReactingTypes = new Set(state.typesReacting); + reactingTypes.forEach(type => { + newReactingTypes.add(type); + }); + + return { + ...state, + typesReacting: Array.from(newReactingTypes), + }; + }, + + [ACTIONS.COMMENT_REACT_COMPLETED]: (state: CommentsState, action: any): CommentsState => { + const reactingTypes = action.data; + const newReactingTypes = new Set(state.typesReacting); + reactingTypes.forEach(type => { + newReactingTypes.delete(type); + }); + + return { + ...state, + typesReacting: Array.from(newReactingTypes), + }; + }, + [ACTIONS.COMMENT_REACTION_LIST_COMPLETED]: (state: CommentsState, action: any): CommentsState => { const { myReactions, othersReactions } = action.data; const myReacts = Object.assign({}, state.myReactsByCommentId); const othersReacts = Object.assign({}, state.othersReactsByCommentId); if (myReactions) { - Object.entries(myReactions).forEach(e => { - myReacts[e[0]] = Object.entries(e[1]).reduce((acc, el) => { - if (el[1] === 1) { - acc.push(el[0]); + Object.entries(myReactions).forEach(([commentId, reactions]) => { + myReacts[commentId] = Object.entries(reactions).reduce((acc, [name, count]) => { + if (count === 1) { + acc.push(name); } return acc; }, []); }); } if (othersReactions) { - Object.entries(othersReactions).forEach(e => { - othersReacts[e[0]] = e[1]; + Object.entries(othersReactions).forEach(([commentId, reactions]) => { + othersReacts[commentId] = reactions; }); } diff --git a/ui/redux/selectors/comments.js b/ui/redux/selectors/comments.js index 26a415937..338b923b6 100644 --- a/ui/redux/selectors/comments.js +++ b/ui/redux/selectors/comments.js @@ -104,6 +104,8 @@ export const makeSelectOthersReactionsForComment = (commentId: string) => return state.othersReactsByCommentId[commentId]; }); +export const selectTypesReacting = createSelector(selectState, state => state.typesReacting); + export const makeSelectCommentsForUri = (uri: string) => createSelector( selectCommentsByClaimId,