diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js index d8388db72..adbecea26 100644 --- a/flow-typed/Comment.js +++ b/flow-typed/Comment.js @@ -37,10 +37,18 @@ declare type CommentsState = { myReactsByCommentId: any, othersReactsByCommentId: any, pendingCommentReactions: Array, - moderationBlockList: ?Array, + moderationBlockList: ?Array, // @KP rename to "personalBlockList"? + adminBlockList: ?Array, + moderatorBlockList: ?Array, + moderatorBlockListDelegatorsMap: {[string]: Array}, // {"blockedUri": ["delegatorUri1", ""delegatorUri2", ...]} fetchingModerationBlockList: boolean, + moderationDelegatesById: { [string]: Array<{ channelId: string, channelName: string }> }, + fetchingModerationDelegates: boolean, + moderationDelegatorsById: { [string]: { global: boolean, delegators: { name: string, claimId: string } }}, + fetchingModerationDelegators: boolean, blockingByUri: {}, unBlockingByUri: {}, + togglingForDelegatorMap: {[string]: Array}, // {"blockedUri": ["delegatorUri1", ""delegatorUri2", ...]} commentsDisabledChannelIds: Array, settingsByChannelId: { [string]: PerChannelSettings }, // ChannelID -> settings fetchingSettings: boolean, @@ -88,6 +96,38 @@ declare type SuperListParams = {}; declare type ModerationBlockParams = {}; +declare type ModerationAddDelegateParams = { + mod_channel_id: string, + mod_channel_name: string, + creator_channel_id: string, + creator_channel_name: string, + signature: string, + signing_ts: string, +}; + +declare type ModerationRemoveDelegateParams = { + mod_channel_id: string, + mod_channel_name: string, + creator_channel_id: string, + creator_channel_name: string, + signature: string, + signing_ts: string, +}; + +declare type ModerationListDelegatesParams = { + creator_channel_id: string, + creator_channel_name: string, + signature: string, + signing_ts: string, +}; + +declare type ModerationAmIParams = { + channel_name: string, + channel_id: string, + signature: string, + signing_ts: string +}; + declare type SettingsParams = { channel_name: string, channel_id: string, diff --git a/ui/comments.js b/ui/comments.js index 0c7f97d04..912c7266e 100644 --- a/ui/comments.js +++ b/ui/comments.js @@ -8,6 +8,12 @@ const Comments = { moderation_block: (params: ModerationBlockParams) => fetchCommentsApi('moderation.Block', params), moderation_unblock: (params: ModerationBlockParams) => fetchCommentsApi('moderation.UnBlock', params), moderation_block_list: (params: ModerationBlockParams) => fetchCommentsApi('moderation.BlockedList', params), + moderation_add_delegate: (params: ModerationAddDelegateParams) => fetchCommentsApi('moderation.AddDelegate', params), + moderation_remove_delegate: (params: ModerationRemoveDelegateParams) => + fetchCommentsApi('moderation.RemoveDelegate', params), + moderation_list_delegates: (params: ModerationListDelegatesParams) => + fetchCommentsApi('moderation.ListDelegates', params), + moderation_am_i: (params: ModerationAmIParams) => fetchCommentsApi('moderation.AmI', params), comment_list: (params: CommentListParams) => fetchCommentsApi('comment.List', params), comment_abandon: (params: CommentAbandonParams) => fetchCommentsApi('comment.Abandon', params), comment_create: (params: CommentCreateParams) => fetchCommentsApi('comment.Create', params), diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js index fffb684f4..5bbc866f0 100644 --- a/ui/constants/action_types.js +++ b/ui/constants/action_types.js @@ -278,6 +278,12 @@ export const COMMENT_MODERATION_BLOCK_FAILED = 'COMMENT_MODERATION_BLOCK_FAILED' export const COMMENT_MODERATION_UN_BLOCK_STARTED = 'COMMENT_MODERATION_UN_BLOCK_STARTED'; export const COMMENT_MODERATION_UN_BLOCK_COMPLETE = 'COMMENT_MODERATION_UN_BLOCK_COMPLETE'; export const COMMENT_MODERATION_UN_BLOCK_FAILED = 'COMMENT_MODERATION_UN_BLOCK_FAILED'; +export const COMMENT_FETCH_MODERATION_DELEGATES_STARTED = 'COMMENT_FETCH_MODERATION_DELEGATES_STARTED'; +export const COMMENT_FETCH_MODERATION_DELEGATES_FAILED = 'COMMENT_FETCH_MODERATION_DELEGATES_FAILED'; +export const COMMENT_FETCH_MODERATION_DELEGATES_COMPLETED = 'COMMENT_FETCH_MODERATION_DELEGATES_COMPLETED'; +export const COMMENT_MODERATION_AM_I_LIST_STARTED = 'COMMENT_MODERATION_AM_I_LIST_STARTED'; +export const COMMENT_MODERATION_AM_I_LIST_FAILED = 'COMMENT_MODERATION_AM_I_LIST_FAILED'; +export const COMMENT_MODERATION_AM_I_LIST_COMPLETED = 'COMMENT_MODERATION_AM_I_LIST_COMPLETED'; export const COMMENT_FETCH_SETTINGS_STARTED = 'COMMENT_FETCH_SETTINGS_STARTED'; export const COMMENT_FETCH_SETTINGS_FAILED = 'COMMENT_FETCH_SETTINGS_FAILED'; export const COMMENT_FETCH_SETTINGS_COMPLETED = 'COMMENT_FETCH_SETTINGS_COMPLETED'; diff --git a/ui/constants/comment.js b/ui/constants/comment.js index 92e8c44c7..f5b9613c0 100644 --- a/ui/constants/comment.js +++ b/ui/constants/comment.js @@ -3,3 +3,9 @@ export const LINKED_COMMENT_QUERY_PARAM = 'lc'; export const SORT_COMMENTS_NEW = 'new'; export const SORT_COMMENTS_BEST = 'best'; export const SORT_COMMENTS_CONTROVERSIAL = 'controversial'; + +export const BLOCK_LEVEL = { + SELF: 'self', + MODERATOR: 'moderator', + ADMIN: 'admin', +}; diff --git a/ui/redux/actions/comments.js b/ui/redux/actions/comments.js index 9868f2f35..d30904438 100644 --- a/ui/redux/actions/comments.js +++ b/ui/redux/actions/comments.js @@ -1,7 +1,8 @@ // @flow import * as ACTIONS from 'constants/action_types'; import * as REACTION_TYPES from 'constants/reactions'; -import { Lbry, parseURI, buildURI, selectClaimsByUri, selectMyChannelClaims } from 'lbry-redux'; +import { BLOCK_LEVEL } from 'constants/comment'; +import { Lbry, parseURI, buildURI, selectClaimsById, selectClaimsByUri, selectMyChannelClaims } from 'lbry-redux'; import { doToast, doSeeNotifications } from 'redux/actions/notifications'; import { makeSelectCommentIdsForUri, @@ -9,6 +10,7 @@ import { makeSelectOthersReactionsForComment, selectPendingCommentReacts, selectModerationBlockList, + selectModerationDelegatorsById, } from 'redux/selectors/comments'; import { makeSelectNotificationForCommentId } from 'redux/selectors/notifications'; import { selectActiveChannelClaim } from 'redux/selectors/app'; @@ -522,42 +524,87 @@ async function channelSignName(channelClaimId: string, channelName: string) { } // Hides a users comments from all creator's claims and prevent them from commenting in the future -export function doCommentModToggleBlock(channelUri: string, showLink: boolean, unblock: boolean = false) { +function doCommentModToggleBlock( + unblock: boolean, + commenterUri: string, + creatorId: string, + blockerIds: Array, // [] = use all my channels + blockLevel: string, + showLink: boolean = false +) { return async (dispatch: Dispatch, getState: GetState) => { const state = getState(); - const myChannels = selectMyChannelClaims(state); - const claim = selectClaimsByUri(state)[channelUri]; - if (!claim) { + let blockerChannelClaims = selectMyChannelClaims(state); + if (blockerIds.length === 0) { + // Specific blockers not provided, so find one based on block-level. + switch (blockLevel) { + case BLOCK_LEVEL.MODERATOR: + { + // Find the first channel that is a moderator for 'creatorId'. + const delegatorsById = selectModerationDelegatorsById(state); + blockerChannelClaims = [ + blockerChannelClaims.find((x) => { + const delegatorDataForId = delegatorsById[x.claim_id]; + return delegatorDataForId && Object.values(delegatorDataForId.delegators).includes(creatorId); + }), + ]; + } + break; + + case BLOCK_LEVEL.ADMIN: + { + // Find the first admin channel and use that. + const delegatorsById = selectModerationDelegatorsById(state); + blockerChannelClaims = [ + blockerChannelClaims.find((x) => delegatorsById[x.claim_id] && delegatorsById[x.claim_id].global), + ]; + } + break; + } + } else { + blockerChannelClaims = blockerChannelClaims.filter((x) => blockerIds.includes(x.claim_id)); + } + + const commenterClaim = selectClaimsByUri(state)[commenterUri]; + if (!commenterClaim) { console.error("Can't find claim to block"); // eslint-disable-line return; } + const creatorClaim = selectClaimsById(state)[creatorId]; + if (creatorId && !creatorClaim) { + console.error("Can't find creator claim"); // eslint-disable-line + return; + } + dispatch({ type: unblock ? ACTIONS.COMMENT_MODERATION_UN_BLOCK_STARTED : ACTIONS.COMMENT_MODERATION_BLOCK_STARTED, data: { - uri: channelUri, + blockedUri: commenterUri, + creatorUri: creatorClaim ? creatorClaim.permanent_url : undefined, + blockLevel: blockLevel, }, }); - const creatorIdForAction = claim ? claim.claim_id : null; - const creatorNameForAction = claim ? claim.name : null; + const commenterIdForAction = commenterClaim ? commenterClaim.claim_id : null; + const commenterNameForAction = commenterClaim ? commenterClaim.name : null; let channelSignatures = []; const sharedModBlockParams = unblock ? { - un_blocked_channel_id: creatorIdForAction, - un_blocked_channel_name: creatorNameForAction, - } + un_blocked_channel_id: commenterIdForAction, + un_blocked_channel_name: commenterNameForAction, + } : { - blocked_channel_id: creatorIdForAction, - blocked_channel_name: creatorNameForAction, - }; + blocked_channel_id: commenterIdForAction, + blocked_channel_name: commenterNameForAction, + }; const commentAction = unblock ? Comments.moderation_unblock : Comments.moderation_block; - return Promise.all(myChannels.map((channel) => channelSignName(channel.claim_id, channel.name))) + return Promise.all(blockerChannelClaims.map((x) => channelSignName(x.claim_id, x.name))) .then((response) => { channelSignatures = response; // $FlowFixMe @@ -566,49 +613,174 @@ export function doCommentModToggleBlock(channelUri: string, showLink: boolean, u .filter((x) => x !== undefined && x !== null) .map((signatureData) => commentAction({ + // $FlowFixMe mod_channel_id: signatureData.claim_id, + // $FlowFixMe mod_channel_name: signatureData.name, + // $FlowFixMe signature: signatureData.signature, + // $FlowFixMe signing_ts: signatureData.signing_ts, + creator_channel_id: creatorClaim ? creatorClaim.claim_id : undefined, + creator_channel_name: creatorClaim ? creatorClaim.name : undefined, + block_all: unblock ? undefined : blockLevel === BLOCK_LEVEL.ADMIN, + global_un_block: unblock ? blockLevel === BLOCK_LEVEL.ADMIN : undefined, ...sharedModBlockParams, }) ) ) - .then(() => { - dispatch({ - type: unblock ? ACTIONS.COMMENT_MODERATION_UN_BLOCK_COMPLETE : ACTIONS.COMMENT_MODERATION_BLOCK_COMPLETE, - data: { channelUri }, + .then((response) => { + const failures = []; + + response.forEach((res, index) => { + if (res.status === 'rejected') { + // TODO: This should be error codes + if (res.reason.message !== 'validation is disallowed for non controlling channels') { + // $FlowFixMe + failures.push(channelSignatures[index].name + ': ' + res.reason.message); + } + } }); - dispatch(doToast({ - message: __(!unblock ? 'Channel blocked. They will not interact with you again.' : 'Channel unblocked!'), - linkText: __(showLink ? 'See All' : ''), - linkTarget: '/settings/block_and_mute', - })); + if (failures.length !== 0) { + dispatch(doToast({ message: failures.join(), isError: true })); + dispatch({ + type: unblock ? ACTIONS.COMMENT_MODERATION_UN_BLOCK_FAILED : ACTIONS.COMMENT_MODERATION_BLOCK_FAILED, + data: { + blockedUri: commenterUri, + creatorUri: creatorClaim ? creatorClaim.permanent_url : undefined, + blockLevel: blockLevel, + }, + }); + return; + } + + dispatch({ + type: unblock ? ACTIONS.COMMENT_MODERATION_UN_BLOCK_COMPLETE : ACTIONS.COMMENT_MODERATION_BLOCK_COMPLETE, + data: { + blockedUri: commenterUri, + creatorUri: creatorClaim ? creatorClaim.permanent_url : undefined, + blockLevel: blockLevel, + }, + }); + + dispatch( + doToast({ + message: unblock + ? __('Channel unblocked!') + : __('Channel "%channel%" blocked.', { channel: commenterNameForAction }), + linkText: __(showLink ? 'See All' : ''), + linkTarget: '/settings/block_and_mute', + }) + ); }) .catch(() => { dispatch({ type: unblock ? ACTIONS.COMMENT_MODERATION_UN_BLOCK_FAILED : ACTIONS.COMMENT_MODERATION_BLOCK_FAILED, + data: { + blockedUri: commenterUri, + creatorUri: creatorClaim ? creatorClaim.permanent_url : undefined, + blockLevel: blockLevel, + }, }); }); }) .catch(() => { dispatch({ type: unblock ? ACTIONS.COMMENT_MODERATION_UN_BLOCK_FAILED : ACTIONS.COMMENT_MODERATION_BLOCK_FAILED, + data: { + blockedUri: commenterUri, + creatorUri: creatorClaim ? creatorClaim.permanent_url : undefined, + blockLevel: blockLevel, + }, }); }); }; } -export function doCommentModBlock(commentAuthor: string, showLink: boolean = true) { +/** + * Blocks the commenter for all channels that I own. + * + * @param commenterUri + * @param showLink + * @returns {function(Dispatch): *} + */ +export function doCommentModBlock(commenterUri: string, showLink: boolean = true) { return (dispatch: Dispatch) => { - return dispatch(doCommentModToggleBlock(commentAuthor, showLink)); + return dispatch(doCommentModToggleBlock(false, commenterUri, '', [], BLOCK_LEVEL.SELF, showLink)); }; } -export function doCommentModUnBlock(commentAuthor: string, showLink: boolean = true) { +/** + * Blocks the commenter using the given channel that has Global privileges. + * + * @param commenterUri + * @param blockerId + * @returns {function(Dispatch): *} + */ +export function doCommentModBlockAsAdmin(commenterUri: string, blockerId: string) { return (dispatch: Dispatch) => { - return dispatch(doCommentModToggleBlock(commentAuthor, showLink, true)); + return dispatch(doCommentModToggleBlock(false, commenterUri, '', blockerId ? [blockerId] : [], BLOCK_LEVEL.ADMIN)); + }; +} + +/** + * Blocks the commenter using the given channel that has been granted + * moderation rights by the creator. + * + * @param commenterUri + * @param creatorId + * @param blockerId + * @returns {function(Dispatch): *} + */ +export function doCommentModBlockAsModerator(commenterUri: string, creatorId: string, blockerId: string) { + return (dispatch: Dispatch) => { + return dispatch( + doCommentModToggleBlock(false, commenterUri, creatorId, blockerId ? [blockerId] : [], BLOCK_LEVEL.MODERATOR) + ); + }; +} + +/** + * Unblocks the commenter for all channels that I own. + * + * @param commenterUri + * @param showLink + * @returns {function(Dispatch): *} + */ +export function doCommentModUnBlock(commenterUri: string, showLink: boolean = true) { + return (dispatch: Dispatch) => { + return dispatch(doCommentModToggleBlock(true, commenterUri, '', [], BLOCK_LEVEL.SELF, showLink)); + }; +} + +/** + * Unblocks the commenter using the given channel that has Global privileges. + * + * @param commenterUri + * @param blockerId + * @returns {function(Dispatch): *} + */ +export function doCommentModUnBlockAsAdmin(commenterUri: string, blockerId: string) { + return (dispatch: Dispatch) => { + return dispatch(doCommentModToggleBlock(true, commenterUri, '', blockerId ? [blockerId] : [], BLOCK_LEVEL.ADMIN)); + }; +} + +/** + * Unblocks the commenter using the given channel that has been granted + * moderation rights by the creator. + * + * @param commenterUri + * @param creatorId + * @param blockerId + * @returns {function(Dispatch): *} + */ +export function doCommentModUnBlockAsModerator(commenterUri: string, creatorId: string, blockerId: string) { + return (dispatch: Dispatch) => { + return dispatch( + doCommentModToggleBlock(true, commenterUri, creatorId, blockerId ? [blockerId] : [], BLOCK_LEVEL.MODERATOR) + ); }; } @@ -640,39 +812,80 @@ export function doFetchModBlockedList() { ) ) .then((res) => { - const blockLists = res.map((r) => r.value); - let globalBlockList = []; - blockLists + let personalBlockList = []; + let adminBlockList = []; + let moderatorBlockList = []; + let moderatorBlockListDelegatorsMap = {}; + + const blockListsPerChannel = res.map((r) => r.value); + blockListsPerChannel .sort((a, b) => { return 1; }) - .forEach((channelBlockListData) => { - const blockListForChannel = channelBlockListData && channelBlockListData.blocked_channels; - if (blockListForChannel) { - blockListForChannel.forEach((blockedChannel) => { - if (blockedChannel.blocked_channel_name) { - const channelUri = buildURI({ - channelName: blockedChannel.blocked_channel_name, - claimId: blockedChannel.blocked_channel_id, - }); + .forEach((channelBlockLists) => { + const storeList = (fetchedList, blockedList, blockedByMap) => { + if (fetchedList) { + fetchedList.forEach((blockedChannel) => { + if (blockedChannel.blocked_channel_name) { + const channelUri = buildURI({ + channelName: blockedChannel.blocked_channel_name, + claimId: blockedChannel.blocked_channel_id, + }); - if (!globalBlockList.find((blockedChannel) => blockedChannel.channelUri === channelUri)) { - globalBlockList.push({ channelUri, blockedAt: blockedChannel.blocked_at }); + if (!blockedList.find((blockedChannel) => blockedChannel.channelUri === channelUri)) { + blockedList.push({ channelUri, blockedAt: blockedChannel.blocked_at }); + } + + if (blockedByMap !== undefined) { + const blockedByChannelUri = buildURI({ + channelName: blockedChannel.blocked_by_channel_name, + claimId: blockedChannel.blocked_by_channel_id, + }); + + if (blockedByMap[channelUri]) { + if (!blockedByMap[channelUri].includes(blockedByChannelUri)) { + blockedByMap[channelUri].push(blockedByChannelUri); + } + } else { + blockedByMap[channelUri] = [blockedByChannelUri]; + } + } } - } - }); - } + }); + } + }; + + const blocked_channels = channelBlockLists && channelBlockLists.blocked_channels; + const globally_blocked_channels = channelBlockLists && channelBlockLists.globally_blocked_channels; + const delegated_blocked_channels = channelBlockLists && channelBlockLists.delegated_blocked_channels; + + storeList(blocked_channels, personalBlockList); + storeList(globally_blocked_channels, adminBlockList); + storeList(delegated_blocked_channels, moderatorBlockList, moderatorBlockListDelegatorsMap); }); dispatch({ type: ACTIONS.COMMENT_MODERATION_BLOCK_LIST_COMPLETED, data: { - blockList: - globalBlockList.length > 0 - ? globalBlockList + personalBlockList: + personalBlockList.length > 0 + ? personalBlockList .sort((a, b) => new Date(a.blockedAt) - new Date(b.blockedAt)) .map((blockedChannel) => blockedChannel.channelUri) : null, + adminBlockList: + adminBlockList.length > 0 + ? adminBlockList + .sort((a, b) => new Date(a.blockedAt) - new Date(b.blockedAt)) + .map((blockedChannel) => blockedChannel.channelUri) + : null, + moderatorBlockList: + moderatorBlockList.length > 0 + ? moderatorBlockList + .sort((a, b) => new Date(a.blockedAt) - new Date(b.blockedAt)) + .map((blockedChannel) => blockedChannel.channelUri) + : null, + moderatorBlockListDelegatorsMap: moderatorBlockListDelegatorsMap, }, }); }) @@ -729,6 +942,190 @@ export const doUpdateBlockListForPublishedChannel = (channelClaim: ChannelClaim) }; }; +export function doCommentModAddDelegate( + modChannelId: string, + modChannelName: string, + creatorChannelClaim: ChannelClaim +) { + return async (dispatch: Dispatch, getState: GetState) => { + let signature: ?{ + signature: string, + signing_ts: string, + }; + try { + signature = await Lbry.channel_sign({ + channel_id: creatorChannelClaim.claim_id, + hexdata: toHex(creatorChannelClaim.name), + }); + } catch (e) {} + + if (!signature) { + return; + } + + return Comments.moderation_add_delegate({ + mod_channel_id: modChannelId, + mod_channel_name: modChannelName, + creator_channel_id: creatorChannelClaim.claim_id, + creator_channel_name: creatorChannelClaim.name, + signature: signature.signature, + signing_ts: signature.signing_ts, + }).catch((err) => { + dispatch( + doToast({ + message: err.message, + isError: true, + }) + ); + }); + }; +} + +export function doCommentModRemoveDelegate( + modChannelId: string, + modChannelName: string, + creatorChannelClaim: ChannelClaim +) { + return async (dispatch: Dispatch, getState: GetState) => { + let signature: ?{ + signature: string, + signing_ts: string, + }; + try { + signature = await Lbry.channel_sign({ + channel_id: creatorChannelClaim.claim_id, + hexdata: toHex(creatorChannelClaim.name), + }); + } catch (e) {} + + if (!signature) { + return; + } + + return Comments.moderation_remove_delegate({ + mod_channel_id: modChannelId, + mod_channel_name: modChannelName, + creator_channel_id: creatorChannelClaim.claim_id, + creator_channel_name: creatorChannelClaim.name, + signature: signature.signature, + signing_ts: signature.signing_ts, + }).catch((err) => { + dispatch( + doToast({ + message: err.message, + isError: true, + }) + ); + }); + }; +} + +export function doCommentModListDelegates(channelClaim: ChannelClaim) { + return async (dispatch: Dispatch, getState: GetState) => { + dispatch({ + type: ACTIONS.COMMENT_FETCH_MODERATION_DELEGATES_STARTED, + }); + + let signature: ?{ + signature: string, + signing_ts: string, + }; + try { + signature = await Lbry.channel_sign({ + channel_id: channelClaim.claim_id, + hexdata: toHex(channelClaim.name), + }); + } catch (e) {} + + if (!signature) { + dispatch({ + type: ACTIONS.COMMENT_FETCH_MODERATION_DELEGATES_FAILED, + }); + return; + } + + return Comments.moderation_list_delegates({ + creator_channel_id: channelClaim.claim_id, + creator_channel_name: channelClaim.name, + signature: signature.signature, + signing_ts: signature.signing_ts, + }) + .then((response) => { + dispatch({ + type: ACTIONS.COMMENT_FETCH_MODERATION_DELEGATES_COMPLETED, + data: { + id: channelClaim.claim_id, + delegates: response.Delegates, + }, + }); + }) + .catch((err) => { + dispatch({ + type: ACTIONS.COMMENT_FETCH_MODERATION_DELEGATES_FAILED, + }); + }); + }; +} + +export function doFetchCommentModAmIList(channelClaim: ChannelClaim) { + return async (dispatch: Dispatch, getState: GetState) => { + const state = getState(); + const myChannels = selectMyChannelClaims(state); + + dispatch({ + type: ACTIONS.COMMENT_MODERATION_AM_I_LIST_STARTED, + }); + + let channelSignatures = []; + + return Promise.all(myChannels.map((channel) => channelSignName(channel.claim_id, channel.name))) + .then((response) => { + channelSignatures = response; + // $FlowFixMe + return Promise.allSettled( + channelSignatures + .filter((x) => x !== undefined && x !== null) + .map((signatureData) => + Comments.moderation_am_i({ + channel_name: signatureData.name, + channel_id: signatureData.claim_id, + signature: signatureData.signature, + signing_ts: signatureData.signing_ts, + }) + ) + ) + .then((res) => { + const delegatorsById = {}; + + channelSignatures.forEach((chanSig, index) => { + if (chanSig && res[index]) { + const value = res[index].value; + delegatorsById[chanSig.claim_id] = { + global: value ? value.type === 'Global' : false, + delegators: value && value.authorized_channels ? value.authorized_channels : {}, + }; + } + }); + + dispatch({ + type: ACTIONS.COMMENT_MODERATION_AM_I_LIST_COMPLETED, + data: delegatorsById, + }); + }) + .catch((err) => { + dispatch({ + type: ACTIONS.COMMENT_MODERATION_AM_I_LIST_FAILED, + }); + }); + }) + .catch(() => { + dispatch({ + type: ACTIONS.COMMENT_MODERATION_AM_I_LIST_FAILED, + }); + }); + }; +} + export const doFetchCreatorSettings = (channelClaimIds: Array = []) => { return async (dispatch: Dispatch, getState: GetState) => { const state = getState(); diff --git a/ui/redux/reducers/comments.js b/ui/redux/reducers/comments.js index 9f9ff9488..a2dc3faad 100644 --- a/ui/redux/reducers/comments.js +++ b/ui/redux/reducers/comments.js @@ -1,6 +1,7 @@ // @flow import * as ACTIONS from 'constants/action_types'; import { handleActions } from 'util/redux-utils'; +import { BLOCK_LEVEL } from 'constants/comment'; const defaultState: CommentsState = { commentById: {}, // commentId -> Comment @@ -21,9 +22,17 @@ const defaultState: CommentsState = { 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, @@ -391,11 +400,14 @@ export default handleActions( fetchingModerationBlockList: true, }), [ACTIONS.COMMENT_MODERATION_BLOCK_LIST_COMPLETED]: (state: CommentsState, action: any) => { - const { blockList } = action.data; + const { personalBlockList, adminBlockList, moderatorBlockList, moderatorBlockListDelegatorsMap } = action.data; return { ...state, - moderationBlockList: blockList, + moderationBlockList: personalBlockList, + adminBlockList: adminBlockList, + moderatorBlockList: moderatorBlockList, + moderatorBlockListDelegatorsMap: moderatorBlockListDelegatorsMap, fetchingModerationBlockList: false, }; }, @@ -404,75 +416,312 @@ export default handleActions( fetchingModerationBlockList: false, }), - [ACTIONS.COMMENT_MODERATION_BLOCK_STARTED]: (state: CommentsState, action: any) => ({ - ...state, - blockingByUri: { - ...state.blockingByUri, - [action.data.uri]: true, - }, - }), + [ACTIONS.COMMENT_MODERATION_BLOCK_STARTED]: (state: CommentsState, action: any) => { + const { blockedUri, creatorUri, blockLevel } = action.data; - [ACTIONS.COMMENT_MODERATION_UN_BLOCK_STARTED]: (state: CommentsState, action: any) => ({ - ...state, - unBlockingByUri: { - ...state.unBlockingByUri, - [action.data.uri]: true, - }, - }), - [ACTIONS.COMMENT_MODERATION_BLOCK_FAILED]: (state: CommentsState, action: any) => ({ - ...state, - blockingByUri: { - ...state.blockingByUri, - [action.data.uri]: false, - }, - }), + switch (blockLevel) { + default: + case BLOCK_LEVEL.SELF: + case BLOCK_LEVEL.ADMIN: + return { + ...state, + blockingByUri: { + ...state.blockingByUri, + [blockedUri]: true, + }, + }; - [ACTIONS.COMMENT_MODERATION_UN_BLOCK_FAILED]: (state: CommentsState, action: any) => ({ - ...state, - unBlockingByUri: { - ...state.unBlockingByUri, - [action.data.uri]: false, - }, - }), + 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 { channelUri } = action.data; + const { blockedUri, creatorUri, blockLevel } = action.data; const commentById = Object.assign({}, state.commentById); const blockingByUri = Object.assign({}, state.blockingByUri); - const moderationBlockList = state.moderationBlockList || []; - const newModerationBlockList = moderationBlockList.slice(); for (const commentId in commentById) { const comment = commentById[commentId]; - if (channelUri === comment.channel_url) { + if (blockedUri === comment.channel_url) { delete commentById[comment.comment_id]; } } - delete blockingByUri[channelUri]; + switch (blockLevel) { + case BLOCK_LEVEL.SELF: { + const blockList = state.moderationBlockList || []; + const newBlockList = blockList.slice(); + newBlockList.push(blockedUri); + delete blockingByUri[blockedUri]; - newModerationBlockList.push(channelUri); + return { + ...state, + commentById, + blockingByUri, + moderationBlockList: newBlockList, + }; + } - return { - ...state, - commentById, - blockingByUri, - moderationBlockList: newModerationBlockList, - }; + 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 { channelUri } = action.data; + const { blockedUri, creatorUri, blockLevel } = action.data; const unBlockingByUri = Object.assign(state.unBlockingByUri, {}); - const moderationBlockList = state.moderationBlockList || []; - const newModerationBlockList = moderationBlockList.slice().filter((uri) => uri !== channelUri); - delete unBlockingByUri[channelUri]; + 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, - unBlockingByUri, - moderationBlockList: newModerationBlockList, + 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, }; }, diff --git a/ui/redux/selectors/comments.js b/ui/redux/selectors/comments.js index 506563bbe..44a60797e 100644 --- a/ui/redux/selectors/comments.js +++ b/ui/redux/selectors/comments.js @@ -16,9 +16,24 @@ export const selectCommentsDisabledChannelIds = createSelector( (state) => state.commentsDisabledChannelIds ); export const selectOthersReactsById = createSelector(selectState, (state) => state.othersReactsByCommentId); + export const selectModerationBlockList = createSelector(selectState, (state) => state.moderationBlockList ? state.moderationBlockList.reverse() : [] ); +export const selectAdminBlockList = createSelector(selectState, (state) => + state.adminBlockList ? state.adminBlockList.reverse() : [] +); +export const selectModeratorBlockList = createSelector(selectState, (state) => + state.moderatorBlockList ? state.moderatorBlockList.reverse() : [] +); + +export const selectModeratorBlockListDelegatorsMap = createSelector( + selectState, + (state) => state.moderatorBlockListDelegatorsMap +); + +export const selectTogglingForDelegatorMap = createSelector(selectState, (state) => state.togglingForDelegatorMap); + export const selectBlockingByUri = createSelector(selectState, (state) => state.blockingByUri); export const selectUnBlockingByUri = createSelector(selectState, (state) => state.unBlockingByUri); export const selectFetchingModerationBlockList = createSelector( @@ -26,6 +41,32 @@ export const selectFetchingModerationBlockList = createSelector( (state) => state.fetchingModerationBlockList ); +export const selectModerationDelegatesById = createSelector(selectState, (state) => state.moderationDelegatesById); +export const selectIsFetchingModerationDelegates = createSelector( + selectState, + (state) => state.fetchingModerationDelegates +); + +export const selectModerationDelegatorsById = createSelector(selectState, (state) => state.moderationDelegatorsById); +export const selectIsFetchingModerationDelegators = createSelector( + selectState, + (state) => state.fetchingModerationDelegators +); + +export const selectHasAdminChannel = createSelector(selectState, (state) => { + const myChannelIds = Object.keys(state.moderationDelegatorsById); + for (let i = 0; i < myChannelIds.length; ++i) { + const id = myChannelIds[i]; + if (state.moderationDelegatorsById[id] && state.moderationDelegatorsById[id].global) { + return true; + } + } + return false; + + /// Lint doesn't like this: + // return Object.values(state.moderationDelegatorsById).some((x) => x.global); +}); + export const selectCommentsByClaimId = createSelector(selectState, selectCommentsById, (state, byId) => { const byClaimId = state.byId || {}; const comments = {}; @@ -298,6 +339,7 @@ export const makeSelectTotalCommentsCountForUri = (uri: string) => return comments ? comments.length : 0; }); +// Personal list export const makeSelectChannelIsBlocked = (uri: string) => createSelector(selectModerationBlockList, (blockedChannelUris) => { if (!blockedChannelUris || !blockedChannelUris) { @@ -307,6 +349,27 @@ export const makeSelectChannelIsBlocked = (uri: string) => return blockedChannelUris.includes(uri); }); +export const makeSelectChannelIsAdminBlocked = (uri: string) => + createSelector(selectAdminBlockList, (list) => { + return list ? list.includes(uri) : false; + }); + +export const makeSelectChannelIsModeratorBlocked = (uri: string) => + createSelector(selectModeratorBlockList, (list) => { + return list ? list.includes(uri) : false; + }); + +export const makeSelectChannelIsModeratorBlockedForCreator = (uri: string, creatorUri: string) => + createSelector(selectModeratorBlockList, selectModeratorBlockListDelegatorsMap, (blockList, delegatorsMap) => { + if (!blockList) return false; + return blockList.includes(uri) && delegatorsMap[uri] && delegatorsMap[uri].includes(creatorUri); + }); + +export const makeSelectIsTogglingForDelegator = (uri: string, creatorUri: string) => + createSelector(selectTogglingForDelegatorMap, (togglingForDelegatorMap) => { + return togglingForDelegatorMap[uri] && togglingForDelegatorMap[uri].includes(creatorUri); + }); + export const makeSelectUriIsBlockingOrUnBlocking = (uri: string) => createSelector(selectBlockingByUri, selectUnBlockingByUri, (blockingByUri, unBlockingByUri) => { return blockingByUri[uri] || unBlockingByUri[uri];