Comments: enable 'enable_comments' flag

## Issue
Closes 6159 "Support Comments Enabled/Disabled for comment.List API"

## New behavior
- `disable-comments` tag will block the comments component entirely.
- `settings.commentsEnabled`:
  - When false, will pause comment fetching, posting and replying.
  - Any already-fetched comments will stay on screen (unless user reloads/F5).
This commit is contained in:
infinite-persistence 2021-06-03 13:57:50 +08:00 committed by jessopb
parent 95fa01a952
commit d6ac2c7954
9 changed files with 95 additions and 27 deletions

View file

@ -41,6 +41,7 @@ declare type CommentsState = {
fetchingModerationBlockList: boolean, fetchingModerationBlockList: boolean,
blockingByUri: {}, blockingByUri: {},
unBlockingByUri: {}, unBlockingByUri: {},
commentsDisabledChannelIds: Array<string>,
settingsByChannelId: { [string]: PerChannelSettings }, // ChannelID -> settings settingsByChannelId: { [string]: PerChannelSettings }, // ChannelID -> settings
fetchingSettings: boolean, fetchingSettings: boolean,
fetchingBlockedWords: boolean, fetchingBlockedWords: boolean,

View file

@ -10,11 +10,13 @@ import { doOpenModal, doSetActiveChannel } from 'redux/actions/app';
import { doCommentCreate } from 'redux/actions/comments'; import { doCommentCreate } from 'redux/actions/comments';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectActiveChannelClaim } from 'redux/selectors/app'; import { selectActiveChannelClaim } from 'redux/selectors/app';
import { makeSelectCommentsDisabledForUri } from 'redux/selectors/comments';
import { doToast } from 'redux/actions/notifications'; import { doToast } from 'redux/actions/notifications';
import { CommentCreate } from './view'; import { CommentCreate } from './view';
const select = (state, props) => ({ const select = (state, props) => ({
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true, commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
commentsDisabledBySettings: makeSelectCommentsDisabledForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
channels: selectMyChannelClaims(state), channels: selectMyChannelClaims(state),
isFetchingChannels: selectFetchingMyChannels(state), isFetchingChannels: selectFetchingMyChannels(state),

View file

@ -15,6 +15,7 @@ import WalletTipAmountSelector from 'component/walletTipAmountSelector';
import CreditAmount from 'component/common/credit-amount'; import CreditAmount from 'component/common/credit-amount';
import ChannelThumbnail from 'component/channelThumbnail'; import ChannelThumbnail from 'component/channelThumbnail';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
import Empty from 'component/common/empty';
const COMMENT_SLOW_MODE_SECONDS = 5; const COMMENT_SLOW_MODE_SECONDS = 5;
@ -22,6 +23,7 @@ type Props = {
uri: string, uri: string,
claim: StreamClaim, claim: StreamClaim,
createComment: (string, string, string, ?string) => Promise<any>, createComment: (string, string, string, ?string) => Promise<any>,
commentsDisabledBySettings: boolean,
channels: ?Array<ChannelClaim>, channels: ?Array<ChannelClaim>,
onDoneReplying?: () => void, onDoneReplying?: () => void,
onCancelReplying?: () => void, onCancelReplying?: () => void,
@ -41,6 +43,7 @@ type Props = {
export function CommentCreate(props: Props) { export function CommentCreate(props: Props) {
const { const {
createComment, createComment,
commentsDisabledBySettings,
claim, claim,
channels, channels,
onDoneReplying, onDoneReplying,
@ -181,6 +184,10 @@ export function CommentCreate(props: Props) {
setAdvancedEditor(!advancedEditor); setAdvancedEditor(!advancedEditor);
} }
if (commentsDisabledBySettings) {
return <Empty padded text={__('This channel has disabled comments on their page.')} />;
}
if (!hasChannels) { if (!hasChannels) {
return ( return (
<div <div

View file

@ -5,6 +5,7 @@ import {
selectIsFetchingComments, selectIsFetchingComments,
makeSelectTotalCommentsCountForUri, makeSelectTotalCommentsCountForUri,
selectOthersReactsById, selectOthersReactsById,
makeSelectCommentsDisabledForUri,
} from 'redux/selectors/comments'; } from 'redux/selectors/comments';
import { doCommentList, doCommentReactList } from 'redux/actions/comments'; import { doCommentList, doCommentReactList } from 'redux/actions/comments';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
@ -18,14 +19,15 @@ const select = (state, props) => ({
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
isFetchingComments: selectIsFetchingComments(state), isFetchingComments: selectIsFetchingComments(state),
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true, commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
commentsDisabledBySettings: makeSelectCommentsDisabledForUri(props.uri)(state),
fetchingChannels: selectFetchingMyChannels(state), fetchingChannels: selectFetchingMyChannels(state),
reactionsById: selectOthersReactsById(state), reactionsById: selectOthersReactsById(state),
activeChannelId: selectActiveChannelId(state), activeChannelId: selectActiveChannelId(state),
}); });
const perform = dispatch => ({ const perform = (dispatch) => ({
fetchComments: uri => dispatch(doCommentList(uri)), fetchComments: (uri) => dispatch(doCommentList(uri)),
fetchReacts: uri => dispatch(doCommentReactList(uri)), fetchReacts: (uri) => dispatch(doCommentReactList(uri)),
}); });
export default connect(select, perform)(CommentsList); export default connect(select, perform)(CommentsList);

View file

@ -16,6 +16,7 @@ import Empty from 'component/common/empty';
type Props = { type Props = {
comments: Array<Comment>, comments: Array<Comment>,
commentsDisabledBySettings: boolean,
fetchComments: (string) => void, fetchComments: (string) => void,
fetchReacts: (string) => Promise<any>, fetchReacts: (string) => Promise<any>,
uri: string, uri: string,
@ -35,6 +36,7 @@ function CommentList(props: Props) {
fetchReacts, fetchReacts,
uri, uri,
comments, comments,
commentsDisabledBySettings,
claimIsMine, claimIsMine,
myChannels, myChannels,
isFetchingComments, isFetchingComments,
@ -147,7 +149,9 @@ function CommentList(props: Props) {
} }
// Default to newest first for apps that don't have comment reactions // Default to newest first for apps that don't have comment reactions
const sortedComments = reactionsById ? sortComments({ comments, reactionsById, sort, isMyComment, justCommented }) : []; const sortedComments = reactionsById
? sortComments({ comments, reactionsById, sort, isMyComment, justCommented })
: [];
const displayedComments = readyToDisplayComments const displayedComments = readyToDisplayComments
? prepareComments(sortedComments, linkedComment).slice(start, end) ? prepareComments(sortedComments, linkedComment).slice(start, end)
: []; : [];
@ -212,7 +216,7 @@ function CommentList(props: Props) {
<> <>
<CommentCreate uri={uri} justCommented={justCommented} /> <CommentCreate uri={uri} justCommented={justCommented} />
{!isFetchingComments && hasNoComments && ( {!commentsDisabledBySettings && !isFetchingComments && hasNoComments && (
<Empty padded text={__('That was pretty deep. What do you think?')} /> <Empty padded text={__('That was pretty deep. What do you think?')} />
)} )}

View file

@ -149,18 +149,18 @@ export default function SettingsCreatorPage(props: Props) {
)} )}
{!isBusy && !isDisabled && ( {!isBusy && !isDisabled && (
<> <>
{FEATURE_IS_READY && ( <Card
<Card title={__('General')}
title={__('General')} actions={
actions={ <>
<> <FormField
<FormField type="checkbox"
type="checkbox" name="comments_enabled"
name="comments_enabled" label={__('Enable comments for channel.')}
label={__('Enable comments for channel.')} checked={commentsEnabled}
checked={commentsEnabled} onChange={() => setSettings({ comments_enabled: !commentsEnabled })}
onChange={() => setSettings({ comments_enabled: !commentsEnabled })} />
/> {FEATURE_IS_READY && (
<FormField <FormField
name="slow_mode_min_gap" name="slow_mode_min_gap"
label={__('Minimum time gap in seconds for Slow Mode in livestream chat.')} label={__('Minimum time gap in seconds for Slow Mode in livestream chat.')}
@ -171,10 +171,10 @@ export default function SettingsCreatorPage(props: Props) {
value={slowModeMinGap} value={slowModeMinGap}
onChange={(e) => setSettings({ slow_mode_min_gap: e.target.value })} onChange={(e) => setSettings({ slow_mode_min_gap: e.target.value })}
/> />
</> )}
} </>
/> }
)} />
<Card <Card
title={__('Filter')} title={__('Filter')}
actions={ actions={

View file

@ -34,10 +34,15 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
type: ACTIONS.COMMENT_LIST_STARTED, type: ACTIONS.COMMENT_LIST_STARTED,
}); });
// Adding 'channel_id' and 'channel_name' enables "CreatorSettings > commentsEnabled".
const authorChannelClaim = claim.value_type === 'channel' ? claim : claim.signing_channel;
return Comments.comment_list({ return Comments.comment_list({
page, page,
claim_id: claimId, claim_id: claimId,
page_size: pageSize, page_size: pageSize,
channel_id: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
channel_name: authorChannelClaim ? authorChannelClaim.name : undefined,
}) })
.then((result: CommentListResponse) => { .then((result: CommentListResponse) => {
const { items: comments } = result; const { items: comments } = result;
@ -46,16 +51,27 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
data: { data: {
comments, comments,
claimId: claimId, claimId: claimId,
authorClaimId: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
uri: uri, uri: uri,
}, },
}); });
return result; return result;
}) })
.catch((error) => { .catch((error) => {
dispatch({ if (error.message === 'comments are disabled by the creator') {
type: ACTIONS.COMMENT_LIST_FAILED, dispatch({
data: error, type: ACTIONS.COMMENT_LIST_COMPLETED,
}); data: {
authorClaimId: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
disabled: true,
},
});
} else {
dispatch({
type: ACTIONS.COMMENT_LIST_FAILED,
data: error,
});
}
}); });
}; };
} }

View file

@ -24,6 +24,7 @@ const defaultState: CommentsState = {
fetchingModerationBlockList: false, fetchingModerationBlockList: false,
blockingByUri: {}, blockingByUri: {},
unBlockingByUri: {}, unBlockingByUri: {},
commentsDisabledChannelIds: [],
settingsByChannelId: {}, // ChannelId -> PerChannelSettings settingsByChannelId: {}, // ChannelId -> PerChannelSettings
fetchingSettings: false, fetchingSettings: false,
fetchingBlockedWords: false, fetchingBlockedWords: false,
@ -167,7 +168,25 @@ export default handleActions(
[ACTIONS.COMMENT_LIST_STARTED]: (state) => ({ ...state, isLoading: true }), [ACTIONS.COMMENT_LIST_STARTED]: (state) => ({ ...state, isLoading: true }),
[ACTIONS.COMMENT_LIST_COMPLETED]: (state: CommentsState, action: any) => { [ACTIONS.COMMENT_LIST_COMPLETED]: (state: CommentsState, action: any) => {
const { comments, claimId, uri } = action.data; const { comments, claimId, uri, disabled, authorClaimId } = action.data;
const commentsDisabledChannelIds = [...state.commentsDisabledChannelIds];
if (disabled) {
if (!commentsDisabledChannelIds.includes(authorClaimId)) {
commentsDisabledChannelIds.push(authorClaimId);
}
return {
...state,
commentsDisabledChannelIds,
isLoading: false,
};
} else {
const index = commentsDisabledChannelIds.indexOf(authorClaimId);
if (index > -1) {
commentsDisabledChannelIds.splice(index, 1);
}
}
const commentById = Object.assign({}, state.commentById); const commentById = Object.assign({}, state.commentById);
const byId = Object.assign({}, state.byId); const byId = Object.assign({}, state.byId);
@ -213,6 +232,7 @@ export default handleActions(
byId, byId,
commentById, commentById,
commentsByUri, commentsByUri,
commentsDisabledChannelIds,
isLoading: false, isLoading: false,
}; };
}, },

View file

@ -3,7 +3,7 @@ import { createSelector } from 'reselect';
import { selectMutedChannels } from 'redux/selectors/blocked'; import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc'; import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
import { selectClaimsById, isClaimNsfw, selectMyActiveClaims } from 'lbry-redux'; import { selectClaimsById, isClaimNsfw, selectMyActiveClaims, makeSelectClaimForUri } from 'lbry-redux';
const selectState = (state) => state.comments || {}; const selectState = (state) => state.comments || {};
@ -11,6 +11,10 @@ export const selectCommentsById = createSelector(selectState, (state) => state.c
export const selectIsFetchingComments = createSelector(selectState, (state) => state.isLoading); export const selectIsFetchingComments = createSelector(selectState, (state) => state.isLoading);
export const selectIsPostingComment = createSelector(selectState, (state) => state.isCommenting); export const selectIsPostingComment = createSelector(selectState, (state) => state.isCommenting);
export const selectIsFetchingReacts = createSelector(selectState, (state) => state.isFetchingReacts); export const selectIsFetchingReacts = createSelector(selectState, (state) => state.isFetchingReacts);
export const selectCommentsDisabledChannelIds = createSelector(
selectState,
(state) => state.commentsDisabledChannelIds
);
export const selectOthersReactsById = createSelector(selectState, (state) => state.othersReactsByCommentId); export const selectOthersReactsById = createSelector(selectState, (state) => state.othersReactsByCommentId);
export const selectModerationBlockList = createSelector(selectState, (state) => export const selectModerationBlockList = createSelector(selectState, (state) =>
state.moderationBlockList ? state.moderationBlockList.reverse() : [] state.moderationBlockList ? state.moderationBlockList.reverse() : []
@ -330,3 +334,15 @@ export const makeSelectSuperChatTotalAmountForUri = (uri: string) =>
return superChatData.totalAmount; return superChatData.totalAmount;
}); });
export const makeSelectCommentsDisabledForUri = (uri: string) =>
createSelector(selectCommentsDisabledChannelIds, makeSelectClaimForUri(uri), (commentsDisabledChannelIds, claim) => {
const channelClaim = !claim
? null
: claim.value_type === 'channel'
? claim
: claim.signing_channel && claim.is_channel_signature_valid
? claim.signing_channel
: null;
return channelClaim && channelClaim.claim_id && commentsDisabledChannelIds.includes(channelClaim.claim_id);
});