// @flow import * as ACTIONS from 'constants/action_types'; import { handleActions } from 'util/redux-utils'; import { BLOCK_LEVEL } from 'constants/comment'; import { isURIEqual } from 'lbry-redux'; const defaultState: CommentsState = { commentById: {}, // commentId -> Comment byId: {}, // ClaimID -> list of fetched comment IDs. totalCommentsById: {}, // ClaimId -> ultimate total (including replies) in commentron. repliesByParentId: {}, // ParentCommentID -> list of fetched replies. totalRepliesByParentId: {}, // ParentCommentID -> total replies in commentron. topLevelCommentsById: {}, // ClaimID -> list of fetched top level comments. topLevelTotalPagesById: {}, // ClaimID -> total number of top-level pages in commentron. Based on COMMENT_PAGE_SIZE_TOP_LEVEL. topLevelTotalCommentsById: {}, // ClaimID -> total top level comments in commentron. // TODO: // Remove commentsByUri // It is not needed and doesn't provide anything but confusion commentsByUri: {}, // URI -> claimId linkedCommentAncestors: {}, // {"linkedCommentId": ["parentId", "grandParentId", ...]} superChatsByUri: {}, pinnedCommentsById: {}, // ClaimId -> array of pinned comment IDs isLoading: false, isLoadingByParentId: {}, isCommenting: false, myComments: undefined, isFetchingReacts: false, pendingCommentReactions: [], typesReacting: [], myReactsByCommentId: undefined, othersReactsByCommentId: undefined, moderationBlockList: undefined, adminBlockList: undefined, moderatorBlockList: undefined, moderatorBlockListDelegatorsMap: {}, fetchingModerationBlockList: false, moderationDelegatesById: {}, fetchingModerationDelegates: false, moderationDelegatorsById: {}, fetchingModerationDelegators: false, blockingByUri: {}, unBlockingByUri: {}, togglingForDelegatorMap: {}, commentsDisabledChannelIds: [], settingsByChannelId: {}, // ChannelId -> PerChannelSettings fetchingSettings: false, fetchingBlockedWords: false, }; function pushToArrayInObject(obj, key, valueToPush) { if (!obj[key]) { obj[key] = [valueToPush]; } else if (!obj[key].includes(valueToPush)) { obj[key].push(valueToPush); } } export default handleActions( { [ACTIONS.COMMENT_CREATE_STARTED]: (state: CommentsState, action: any): CommentsState => ({ ...state, isCommenting: true, }), [ACTIONS.COMMENT_CREATE_FAILED]: (state: CommentsState, action: any) => ({ ...state, isCommenting: false, }), [ACTIONS.COMMENT_CREATE_COMPLETED]: (state: CommentsState, action: any): CommentsState => { const { comment, claimId, uri, livestream, }: { comment: Comment, claimId: string, uri: string, livestream: boolean } = action.data; const commentById = Object.assign({}, state.commentById); const byId = Object.assign({}, state.byId); const totalCommentsById = Object.assign({}, state.totalCommentsById); const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]} const repliesByParentId = Object.assign({}, state.repliesByParentId); // {ParentCommentID -> [commentIds...] } list of reply comments const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId); const commentsByUri = Object.assign({}, state.commentsByUri); const comments = byId[claimId] || []; const newCommentIds = comments.slice(); // If it was created during a livestream, let the websocket handler perform the state update if (!livestream) { // add the comment by its ID commentById[comment.comment_id] = comment; // push the comment_id to the top of ID list newCommentIds.unshift(comment.comment_id); byId[claimId] = newCommentIds; if (totalCommentsById[claimId]) { totalCommentsById[claimId] += 1; } if (comment['parent_id']) { if (!repliesByParentId[comment.parent_id]) { repliesByParentId[comment.parent_id] = [comment.comment_id]; } else { repliesByParentId[comment.parent_id].unshift(comment.comment_id); } if (!totalRepliesByParentId[comment.parent_id]) { totalRepliesByParentId[comment.parent_id] = 1; } else { totalRepliesByParentId[comment.parent_id] += 1; } // Update the parent's "replies" value if (commentById[comment.parent_id]) { commentById[comment.parent_id].replies = (commentById[comment.parent_id].replies || 0) + 1; } } else { if (!topLevelCommentsById[claimId]) { commentsByUri[uri] = claimId; topLevelCommentsById[claimId] = [comment.comment_id]; } else { topLevelCommentsById[claimId].unshift(comment.comment_id); } } } return { ...state, topLevelCommentsById, repliesByParentId, totalRepliesByParentId, commentById, byId, totalCommentsById, commentsByUri, isLoading: false, isCommenting: false, }; }, [ACTIONS.COMMENT_REACTION_LIST_STARTED]: (state: CommentsState, action: any): CommentsState => ({ ...state, isFetchingReacts: true, }), [ACTIONS.COMMENT_REACTION_LIST_FAILED]: (state: CommentsState, action: any) => ({ ...state, isFetchingReacts: false, }), [ACTIONS.COMMENT_REACT_FAILED]: (state: CommentsState, action: any): CommentsState => { const commentReaction = action.data; // String: reactionHash + type const newReactingTypes = new Set(state.pendingCommentReactions); newReactingTypes.delete(commentReaction); return { ...state, pendingCommentReactions: Array.from(newReactingTypes), }; }, [ACTIONS.COMMENT_REACT_STARTED]: (state: CommentsState, action: any): CommentsState => { const commentReaction = action.data; const newReactingTypes = new Set(state.pendingCommentReactions); newReactingTypes.add(commentReaction); return { ...state, pendingCommentReactions: Array.from(newReactingTypes), }; }, [ACTIONS.COMMENT_REACT_COMPLETED]: (state: CommentsState, action: any): CommentsState => { const commentReaction = action.data; // String: reactionHash + type const newReactingTypes = new Set(state.pendingCommentReactions); newReactingTypes.delete(commentReaction); return { ...state, pendingCommentReactions: Array.from(newReactingTypes), }; }, [ACTIONS.COMMENT_REACTION_LIST_COMPLETED]: (state: CommentsState, action: any): CommentsState => { const { myReactions, othersReactions, channelId, commentIds } = action.data; const myReacts = Object.assign({}, state.myReactsByCommentId); const othersReacts = Object.assign({}, state.othersReactsByCommentId); const myReactionsEntries = myReactions ? Object.entries(myReactions) : []; const othersReactionsEntries = othersReactions ? Object.entries(othersReactions) : []; if (myReactionsEntries.length > 0) { myReactionsEntries.forEach(([commentId, reactions]) => { const key = channelId ? `${commentId}:${channelId}` : commentId; myReacts[key] = Object.entries(reactions).reduce((acc, [name, count]) => { if (count === 1) { acc.push(name); } return acc; }, []); }); } else { commentIds.forEach((commentId) => { const key = channelId ? `${commentId}:${channelId}` : commentId; myReacts[key] = []; }); } if (othersReactionsEntries.length > 0) { othersReactionsEntries.forEach(([commentId, reactions]) => { const key = channelId ? `${commentId}:${channelId}` : commentId; othersReacts[key] = reactions; }); } else { commentIds.forEach((commentId) => { const key = channelId ? `${commentId}:${channelId}` : commentId; othersReacts[key] = {}; }); } return { ...state, isFetchingReacts: false, myReactsByCommentId: myReacts, othersReactsByCommentId: othersReacts, }; }, [ACTIONS.COMMENT_LIST_STARTED]: (state, action: any) => { const { parentId } = action.data; const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId); if (parentId) { isLoadingByParentId[parentId] = true; } return { ...state, isLoading: true, isLoadingByParentId, }; }, [ACTIONS.COMMENT_LIST_COMPLETED]: (state: CommentsState, action: any) => { const { comments, parentId, totalItems, totalFilteredItems, totalPages, claimId, uri, disabled, authorClaimId, } = action.data; const commentsDisabledChannelIds = [...state.commentsDisabledChannelIds]; if (disabled) { if (!commentsDisabledChannelIds.includes(authorClaimId)) { commentsDisabledChannelIds.push(authorClaimId); } const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId); if (parentId) { isLoadingByParentId[parentId] = false; } return { ...state, commentsDisabledChannelIds, isLoading: false, isLoadingByParentId, }; } else { const index = commentsDisabledChannelIds.indexOf(authorClaimId); if (index > -1) { commentsDisabledChannelIds.splice(index, 1); } } const commentById = Object.assign({}, state.commentById); const byId = Object.assign({}, state.byId); const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]} const topLevelTotalCommentsById = Object.assign({}, state.topLevelTotalCommentsById); const topLevelTotalPagesById = Object.assign({}, state.topLevelTotalPagesById); const commentsByUri = Object.assign({}, state.commentsByUri); const repliesByParentId = Object.assign({}, state.repliesByParentId); const totalCommentsById = Object.assign({}, state.totalCommentsById); const pinnedCommentsById = Object.assign({}, state.pinnedCommentsById); const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId); const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId); if (!parentId) { totalCommentsById[claimId] = totalItems; topLevelTotalCommentsById[claimId] = totalFilteredItems; topLevelTotalPagesById[claimId] = totalPages; } else { totalRepliesByParentId[parentId] = totalFilteredItems; isLoadingByParentId[parentId] = false; } const commonUpdateAction = (comment, commentById, commentIds, index) => { // map the comment_ids to the new comments commentById[comment.comment_id] = comment; commentIds[index] = comment.comment_id; }; if (comments) { // we use an Array to preserve order of listing // in reality this doesn't matter and we can just // sort comments by their timestamp const commentIds = Array(comments.length); // --- Top-level comments --- if (!parentId) { for (let i = 0; i < comments.length; ++i) { const comment = comments[i]; commonUpdateAction(comment, commentById, commentIds, i); pushToArrayInObject(topLevelCommentsById, claimId, comment.comment_id); if (comment.is_pinned) { pushToArrayInObject(pinnedCommentsById, claimId, comment.comment_id); } } } // --- Replies --- else { for (let i = 0; i < comments.length; ++i) { const comment = comments[i]; commonUpdateAction(comment, commentById, commentIds, i); pushToArrayInObject(repliesByParentId, parentId, comment.comment_id); } } byId[claimId] ? byId[claimId].push(...commentIds) : (byId[claimId] = commentIds); commentsByUri[uri] = claimId; } return { ...state, topLevelCommentsById, topLevelTotalCommentsById, topLevelTotalPagesById, repliesByParentId, totalCommentsById, pinnedCommentsById, totalRepliesByParentId, byId, commentById, commentsByUri, commentsDisabledChannelIds, isLoading: false, isLoadingByParentId, }; }, [ACTIONS.COMMENT_BY_ID_COMPLETED]: (state: CommentsState, action: any) => { const { comment, ancestors } = action.data; const claimId = comment.claim_id; const commentById = Object.assign({}, state.commentById); const byId = Object.assign({}, state.byId); const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]} const topLevelTotalCommentsById = Object.assign({}, state.topLevelTotalCommentsById); const topLevelTotalPagesById = Object.assign({}, state.topLevelTotalPagesById); const repliesByParentId = Object.assign({}, state.repliesByParentId); const linkedCommentAncestors = Object.assign({}, state.linkedCommentAncestors); const updateStore = (comment, commentById, byId, repliesByParentId, topLevelCommentsById) => { // 'comment.ByID' doesn't populate 'replies'. We should have at least 1 // at the moment, and the correct value will populated by 'comment.List'. commentById[comment.comment_id] = { ...comment, replies: 1 }; byId[claimId] ? byId[claimId].unshift(comment.comment_id) : (byId[claimId] = [comment.comment_id]); const parentId = comment.parent_id; if (comment.parent_id) { pushToArrayInObject(repliesByParentId, parentId, comment.comment_id); } else { pushToArrayInObject(topLevelCommentsById, claimId, comment.comment_id); } }; updateStore(comment, commentById, byId, repliesByParentId, topLevelCommentsById); if (ancestors) { ancestors.forEach((ancestor) => { updateStore(ancestor, commentById, byId, repliesByParentId, topLevelCommentsById); pushToArrayInObject(linkedCommentAncestors, comment.comment_id, ancestor.comment_id); }); } return { ...state, topLevelCommentsById, topLevelTotalCommentsById, topLevelTotalPagesById, repliesByParentId, byId, commentById, linkedCommentAncestors, }; }, [ACTIONS.COMMENT_SUPER_CHAT_LIST_FAILED]: (state: CommentsState, action: any) => ({ ...state, isLoading: false, }), [ACTIONS.COMMENT_SUPER_CHAT_LIST_STARTED]: (state) => ({ ...state, isLoading: true }), [ACTIONS.COMMENT_SUPER_CHAT_LIST_COMPLETED]: (state: CommentsState, action: any) => { const { comments, totalAmount, uri } = action.data; return { ...state, superChatsByUri: { ...state.superChatsByUri, [uri]: { comments, totalAmount, }, }, isLoading: false, }; }, [ACTIONS.COMMENT_LIST_FAILED]: (state: CommentsState, action: any) => ({ ...state, isLoading: false, }), [ACTIONS.COMMENT_LIST_RESET]: (state: CommentsState, action: any) => { const { claimId } = action.data; const byId = Object.assign({}, state.byId); const totalCommentsById = Object.assign({}, state.totalCommentsById); const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]} const topLevelTotalCommentsById = Object.assign({}, state.topLevelTotalCommentsById); const topLevelTotalPagesById = Object.assign({}, state.topLevelTotalPagesById); const myReacts = Object.assign({}, state.myReactsByCommentId); const othersReacts = Object.assign({}, state.othersReactsByCommentId); function deleteReacts(reactObj, commentIdsToRemove) { if (commentIdsToRemove && commentIdsToRemove.length > 0) { let reactionKeys = Object.keys(reactObj); reactionKeys.forEach((rk) => { const colonIndex = rk.indexOf(':'); const commentId = colonIndex === -1 ? rk : rk.substring(0, colonIndex); if (commentIdsToRemove.includes(commentId)) { delete reactObj[rk]; } }); } } deleteReacts(myReacts, byId[claimId]); deleteReacts(othersReacts, byId[claimId]); delete byId[claimId]; delete totalCommentsById[claimId]; delete topLevelCommentsById[claimId]; delete topLevelTotalCommentsById[claimId]; delete topLevelTotalPagesById[claimId]; return { ...state, byId, totalCommentsById, topLevelCommentsById, topLevelTotalCommentsById, topLevelTotalPagesById, myReactsByCommentId: myReacts, othersReactsByCommentId: othersReacts, }; }, [ACTIONS.COMMENT_RECEIVED]: (state: CommentsState, action: any) => { const { uri, claimId, comment } = action.data; const commentsByUri = Object.assign({}, state.commentsByUri); const commentsByClaimId = Object.assign({}, state.byId); const allCommentsById = Object.assign({}, state.commentById); const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); const superChatsByUri = Object.assign({}, state.superChatsByUri); const commentsForId = topLevelCommentsById[claimId]; allCommentsById[comment.comment_id] = comment; commentsByUri[uri] = claimId; if (commentsForId) { const newCommentsForId = commentsForId.slice(); const commentExists = newCommentsForId.includes(comment.comment_id); if (!commentExists) { newCommentsForId.unshift(comment.comment_id); } topLevelCommentsById[claimId] = newCommentsForId; } else { topLevelCommentsById[claimId] = [comment.comment_id]; } // We don't care to keep existing lower level comments since this is just for livestreams commentsByClaimId[claimId] = topLevelCommentsById[claimId]; if (comment.support_amount > 0) { const superChatForUri = superChatsByUri[uri]; const superChatCommentsForUri = superChatForUri && superChatForUri.comments; let sortedSuperChatComments = []; let hasAddedNewComment = false; if (superChatCommentsForUri && superChatCommentsForUri.length > 0) { // Go for the entire length of superChatCommentsForUri since a comment will be added to this list for (var i = 0; i < superChatCommentsForUri.length; i++) { const existingSuperChat = superChatCommentsForUri[i]; if (existingSuperChat.support_amount < comment.support_amount && !hasAddedNewComment) { hasAddedNewComment = true; sortedSuperChatComments.push(comment); sortedSuperChatComments.push(existingSuperChat); } else { sortedSuperChatComments.push(existingSuperChat); } // If the new superchat hasn't been added yet, it must be the smallest superchat in the list if ( i === superChatCommentsForUri.length - 1 && sortedSuperChatComments.length === superChatCommentsForUri.length ) { sortedSuperChatComments.push(comment); } } superChatsByUri[uri].comments = sortedSuperChatComments; superChatsByUri[uri].totalAmount += comment.support_amount; } else { superChatsByUri[uri] = { comments: [comment], totalAmount: comment.support_amount }; } } return { ...state, byId: commentsByClaimId, commentById: allCommentsById, commentsByUri, topLevelCommentsById, superChatsByUri, }; }, [ACTIONS.COMMENT_ABANDON_STARTED]: (state: CommentsState, action: any) => ({ ...state, isLoading: true, }), [ACTIONS.COMMENT_ABANDON_COMPLETED]: (state: CommentsState, action: any) => { const { comment_id } = action.data; const commentById = Object.assign({}, state.commentById); const byId = Object.assign({}, state.byId); const repliesByParentId = Object.assign({}, state.repliesByParentId); // {ParentCommentID -> [commentIds...] } list of reply comments const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId); const totalCommentsById = Object.assign({}, state.totalCommentsById); const comment = commentById[comment_id]; // to remove the comment and its references const claimId = comment.claim_id; for (let i = 0; i < byId[claimId].length; i++) { if (byId[claimId][i] === comment_id) { byId[claimId].splice(i, 1); break; } } // Update replies if (comment['parent_id'] && repliesByParentId[comment.parent_id]) { const index = repliesByParentId[comment.parent_id].indexOf(comment.comment_id); if (index > -1) { repliesByParentId[comment.parent_id].splice(index, 1); if (commentById[comment.parent_id]) { commentById[comment.parent_id].replies = Math.max(0, (commentById[comment.parent_id].replies || 0) - 1); } if (totalRepliesByParentId[comment.parent_id]) { totalRepliesByParentId[comment.parent_id] = Math.max(0, totalRepliesByParentId[comment.parent_id] - 1); } } } if (totalCommentsById[claimId]) { totalCommentsById[claimId] = Math.max(0, totalCommentsById[claimId] - 1); } delete commentById[comment_id]; return { ...state, commentById, byId, totalCommentsById, repliesByParentId, totalRepliesByParentId, isLoading: false, }; }, [ACTIONS.COMMENT_ABANDON_FAILED]: (state: CommentsState, action: any) => ({ ...state, isCommenting: false, }), [ACTIONS.COMMENT_UPDATE_STARTED]: (state: CommentsState, action: any) => ({ ...state, isCommenting: true, }), [ACTIONS.COMMENT_UPDATE_COMPLETED]: (state: CommentsState, action: any) => { const { comment } = action.data; const commentById = Object.assign({}, state.commentById); commentById[comment.comment_id] = comment; return { ...state, commentById, isCommenting: false, }; }, [ACTIONS.COMMENT_UPDATE_FAILED]: (state: CommentsState, action: any) => ({ ...state, isCmmenting: false, }), [ACTIONS.COMMENT_PIN_COMPLETED]: (state: CommentsState, action: any) => { const { pinnedComment, claimId, unpin } = action.data; const commentById = Object.assign({}, state.commentById); const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); const pinnedCommentsById = Object.assign({}, state.pinnedCommentsById); if (pinnedComment && topLevelCommentsById[claimId]) { const index = topLevelCommentsById[claimId].indexOf(pinnedComment.comment_id); if (index > -1) { topLevelCommentsById[claimId].splice(index, 1); if (pinnedCommentsById[claimId]) { // Remove here so that the 'unshift' below will be a unique entry. pinnedCommentsById[claimId] = pinnedCommentsById[claimId].filter((x) => x !== pinnedComment.comment_id); } else { pinnedCommentsById[claimId] = []; } if (unpin) { // Without the sort score, I have no idea where to put it. Just // dump it at the bottom. Users can refresh if they want it back to // the correct sorted position. topLevelCommentsById[claimId].push(pinnedComment.comment_id); } else { topLevelCommentsById[claimId].unshift(pinnedComment.comment_id); pinnedCommentsById[claimId].unshift(pinnedComment.comment_id); } if (commentById[pinnedComment.comment_id]) { // Commentron's `comment.Pin` response places the creator's credentials // in the 'channel_*' fields, which doesn't make sense. Maybe it is to // show who signed/pinned it, but even if so, it shouldn't overload // these variables which are already used by existing comment data structure. // Ensure we don't override the existing/correct values, but fallback // to whatever was given. const { channel_id, channel_name, channel_url } = commentById[pinnedComment.comment_id]; commentById[pinnedComment.comment_id] = { ...pinnedComment, channel_id: channel_id || pinnedComment.channel_id, channel_name: channel_name || pinnedComment.channel_name, channel_url: channel_url || pinnedComment.channel_url, }; } else { commentById[pinnedComment.comment_id] = pinnedComment; } } } return { ...state, commentById, topLevelCommentsById, pinnedCommentsById, }; }, [ACTIONS.COMMENT_MODERATION_BLOCK_LIST_STARTED]: (state: CommentsState, action: any) => ({ ...state, fetchingModerationBlockList: true, }), [ACTIONS.COMMENT_MODERATION_BLOCK_LIST_COMPLETED]: (state: CommentsState, action: any) => { const { personalBlockList, adminBlockList, moderatorBlockList, moderatorBlockListDelegatorsMap } = action.data; return { ...state, moderationBlockList: personalBlockList, adminBlockList: adminBlockList, moderatorBlockList: moderatorBlockList, moderatorBlockListDelegatorsMap: moderatorBlockListDelegatorsMap, fetchingModerationBlockList: false, }; }, [ACTIONS.COMMENT_MODERATION_BLOCK_LIST_FAILED]: (state: CommentsState, action: any) => ({ ...state, fetchingModerationBlockList: false, }), [ACTIONS.COMMENT_MODERATION_BLOCK_STARTED]: (state: CommentsState, action: any) => { const { blockedUri, creatorUri, blockLevel } = action.data; switch (blockLevel) { default: case BLOCK_LEVEL.SELF: case BLOCK_LEVEL.ADMIN: return { ...state, blockingByUri: { ...state.blockingByUri, [blockedUri]: true, }, }; case BLOCK_LEVEL.MODERATOR: const newMap = Object.assign({}, state.togglingForDelegatorMap); const togglingDelegatorsForBlockedUri = newMap[blockedUri]; if (togglingDelegatorsForBlockedUri) { if (!togglingDelegatorsForBlockedUri.includes(creatorUri)) { togglingDelegatorsForBlockedUri.push(creatorUri); } } else { newMap[blockedUri] = [creatorUri]; } return { ...state, togglingForDelegatorMap: newMap, }; } }, [ACTIONS.COMMENT_MODERATION_UN_BLOCK_STARTED]: (state: CommentsState, action: any) => { const { blockedUri, creatorUri, blockLevel } = action.data; switch (blockLevel) { default: case BLOCK_LEVEL.SELF: case BLOCK_LEVEL.ADMIN: return { ...state, unBlockingByUri: { ...state.unBlockingByUri, [blockedUri]: true, }, }; case BLOCK_LEVEL.MODERATOR: const newMap = Object.assign({}, state.togglingForDelegatorMap); const togglingDelegatorsForBlockedUri = newMap[blockedUri]; if (togglingDelegatorsForBlockedUri) { if (!togglingDelegatorsForBlockedUri.includes(creatorUri)) { togglingDelegatorsForBlockedUri.push(creatorUri); } } else { newMap[blockedUri] = [creatorUri]; } return { ...state, togglingForDelegatorMap: newMap, }; } }, [ACTIONS.COMMENT_MODERATION_BLOCK_FAILED]: (state: CommentsState, action: any) => { const { blockedUri, creatorUri, blockLevel } = action.data; switch (blockLevel) { default: case BLOCK_LEVEL.SELF: case BLOCK_LEVEL.ADMIN: return { ...state, blockingByUri: { ...state.blockingByUri, [blockedUri]: false, }, }; case BLOCK_LEVEL.MODERATOR: const newMap = Object.assign({}, state.togglingForDelegatorMap); const togglingDelegatorsForBlockedUri = newMap[blockedUri]; if (togglingDelegatorsForBlockedUri) { newMap[blockedUri] = togglingDelegatorsForBlockedUri.filter((x) => x !== creatorUri); } return { ...state, togglingForDelegatorMap: newMap, }; } }, [ACTIONS.COMMENT_MODERATION_UN_BLOCK_FAILED]: (state: CommentsState, action: any) => { const { blockedUri, creatorUri, blockLevel } = action.data; switch (blockLevel) { default: case BLOCK_LEVEL.SELF: case BLOCK_LEVEL.ADMIN: return { ...state, unBlockingByUri: { ...state.unBlockingByUri, [blockedUri]: false, }, }; case BLOCK_LEVEL.MODERATOR: const newMap = Object.assign({}, state.togglingForDelegatorMap); const togglingDelegatorsForBlockedUri = newMap[blockedUri]; if (togglingDelegatorsForBlockedUri) { newMap[blockedUri] = togglingDelegatorsForBlockedUri.filter((x) => x !== creatorUri); } return { ...state, togglingForDelegatorMap: newMap, }; } }, [ACTIONS.COMMENT_MODERATION_BLOCK_COMPLETE]: (state: CommentsState, action: any) => { const { blockedUri, creatorUri, blockLevel } = action.data; const commentById = Object.assign({}, state.commentById); const blockingByUri = Object.assign({}, state.blockingByUri); for (const commentId in commentById) { const comment = commentById[commentId]; if (isURIEqual(blockedUri, comment.channel_url)) { delete commentById[comment.comment_id]; } } switch (blockLevel) { case BLOCK_LEVEL.SELF: { const blockList = state.moderationBlockList || []; const newBlockList = blockList.slice(); newBlockList.push(blockedUri); delete blockingByUri[blockedUri]; return { ...state, commentById, blockingByUri, moderationBlockList: newBlockList, }; } case BLOCK_LEVEL.MODERATOR: { const blockList = state.moderatorBlockList || []; const newBlockList = blockList.slice(); // Update main block list if (!newBlockList.includes(blockedUri)) { newBlockList.push(blockedUri); } // Update list of delegators const moderatorBlockListDelegatorsMap = Object.assign({}, state.moderatorBlockListDelegatorsMap); const delegatorUrisForBlockedUri = moderatorBlockListDelegatorsMap[blockedUri]; if (delegatorUrisForBlockedUri) { if (!delegatorUrisForBlockedUri.includes(creatorUri)) { delegatorUrisForBlockedUri.push(creatorUri); } } else { moderatorBlockListDelegatorsMap[blockedUri] = [creatorUri]; } // Remove "toggling" flag const togglingMap = Object.assign({}, state.togglingForDelegatorMap); const togglingDelegatorsForBlockedUri = togglingMap[blockedUri]; if (togglingDelegatorsForBlockedUri) { togglingMap[blockedUri] = togglingDelegatorsForBlockedUri.filter((x) => x !== creatorUri); } return { ...state, commentById, moderatorBlockList: newBlockList, moderatorBlockListDelegatorsMap, togglingForDelegatorMap: togglingMap, }; } case BLOCK_LEVEL.ADMIN: const blockList = state.adminBlockList || []; const newBlockList = blockList.slice(); newBlockList.push(blockedUri); delete blockingByUri[blockedUri]; return { ...state, commentById, blockingByUri, adminBlockList: newBlockList, }; } }, [ACTIONS.COMMENT_MODERATION_UN_BLOCK_COMPLETE]: (state: CommentsState, action: any) => { const { blockedUri, creatorUri, blockLevel } = action.data; const unBlockingByUri = Object.assign(state.unBlockingByUri, {}); switch (blockLevel) { case BLOCK_LEVEL.SELF: { const blockList = state.moderationBlockList || []; delete unBlockingByUri[blockedUri]; return { ...state, unBlockingByUri, moderationBlockList: blockList.slice().filter((uri) => uri !== blockedUri), }; } case BLOCK_LEVEL.ADMIN: { const blockList = state.adminBlockList || []; delete unBlockingByUri[blockedUri]; return { ...state, unBlockingByUri, adminBlockList: blockList.slice().filter((uri) => uri !== blockedUri), }; } case BLOCK_LEVEL.MODERATOR: { const blockList = state.moderatorBlockList || []; const newBlockList = blockList.slice(); const togglingMap = Object.assign({}, state.togglingForDelegatorMap); const moderatorBlockListDelegatorsMap = Object.assign({}, state.moderatorBlockListDelegatorsMap); const delegatorUrisForBlockedUri = moderatorBlockListDelegatorsMap[blockedUri]; if (delegatorUrisForBlockedUri) { const index = delegatorUrisForBlockedUri.indexOf(creatorUri); if (index > -1) { // Remove from delegators list delegatorUrisForBlockedUri.splice(index, 1); // // Remove blocked entry if it was removed for all delegators // if (delegatorUrisForBlockedUri.length === 0) { // delete moderatorBlockListDelegatorsMap[blockedUri]; // newBlockList = newBlockList.filter((uri) => uri !== blockedUri); // } // Remove from "toggling" flag const togglingDelegatorsForBlockedUri = togglingMap[blockedUri]; if (togglingDelegatorsForBlockedUri) { togglingMap[blockedUri] = togglingDelegatorsForBlockedUri.filter((x) => x !== creatorUri); } } } return { ...state, moderatorBlockList: newBlockList, togglingForDelegatorMap: togglingMap, }; } } }, [ACTIONS.COMMENT_FETCH_MODERATION_DELEGATES_STARTED]: (state: CommentsState, action: any) => ({ ...state, fetchingModerationDelegates: true, }), [ACTIONS.COMMENT_FETCH_MODERATION_DELEGATES_FAILED]: (state: CommentsState, action: any) => ({ ...state, fetchingModerationDelegates: false, }), [ACTIONS.COMMENT_FETCH_MODERATION_DELEGATES_COMPLETED]: (state: CommentsState, action: any) => { const moderationDelegatesById = Object.assign({}, state.moderationDelegatesById); if (action.data.delegates) { moderationDelegatesById[action.data.id] = action.data.delegates.map((delegate) => { return { channelId: delegate.channel_id, channelName: delegate.channel_name, }; }); } else { moderationDelegatesById[action.data.id] = []; } return { ...state, fetchingModerationDelegates: false, moderationDelegatesById: moderationDelegatesById, }; }, [ACTIONS.COMMENT_MODERATION_AM_I_LIST_STARTED]: (state: CommentsState, action: any) => ({ ...state, fetchingModerationDelegators: true, }), [ACTIONS.COMMENT_MODERATION_AM_I_LIST_FAILED]: (state: CommentsState, action: any) => ({ ...state, fetchingModerationDelegators: true, }), [ACTIONS.COMMENT_MODERATION_AM_I_LIST_COMPLETED]: (state: CommentsState, action: any) => { return { ...state, fetchingModerationDelegators: true, moderationDelegatorsById: action.data, }; }, [ACTIONS.COMMENT_FETCH_SETTINGS_STARTED]: (state: CommentsState, action: any) => ({ ...state, fetchingSettings: true, }), [ACTIONS.COMMENT_FETCH_SETTINGS_FAILED]: (state: CommentsState, action: any) => ({ ...state, fetchingSettings: false, }), [ACTIONS.COMMENT_FETCH_SETTINGS_COMPLETED]: (state: CommentsState, action: any) => { // TODO: This is incorrect, as it could make 'settingsByChannelId' store // only 1 channel with other channel's data purged. It works for now // because the GUI only shows 1 channel's setting at a time, and *always* // re-fetches to get latest data before displaying. Either rename this to // 'activeChannelCreatorSettings', or append the new data properly. const settingsByChannelId = Object.assign({}, action.data); const BTC_SATOSHIS = 100000000; const channelIds = Object.keys(settingsByChannelId); channelIds.forEach((ci) => { if (settingsByChannelId[ci].min_tip_amount_comment) { settingsByChannelId[ci].min_tip_amount_comment /= BTC_SATOSHIS; } if (settingsByChannelId[ci].min_tip_amount_super_chat) { settingsByChannelId[ci].min_tip_amount_super_chat /= BTC_SATOSHIS; } }); return { ...state, settingsByChannelId, fetchingSettings: false, }; }, [ACTIONS.COMMENT_FETCH_BLOCKED_WORDS_STARTED]: (state: CommentsState, action: any) => ({ ...state, fetchingBlockedWords: true, }), [ACTIONS.COMMENT_FETCH_BLOCKED_WORDS_FAILED]: (state: CommentsState, action: any) => ({ ...state, fetchingBlockedWords: false, }), [ACTIONS.COMMENT_FETCH_BLOCKED_WORDS_COMPLETED]: (state: CommentsState, action: any) => { const blockedWordsByChannelId = action.data; const settingsByChannelId = Object.assign({}, state.settingsByChannelId); // blockedWordsByChannelId: {string: [string]} Object.entries(blockedWordsByChannelId).forEach((x) => { const channelId = x[0]; if (!settingsByChannelId[channelId]) { settingsByChannelId[channelId] = {}; } settingsByChannelId[channelId].words = x[1]; }); return { ...state, settingsByChannelId, fetchingBlockedWords: false, }; }, }, defaultState );