From a05ccdd44fed0c9caf0c413a35acfd1305fec9e7 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Thu, 12 Aug 2021 15:10:44 +0800 Subject: [PATCH 1/7] Comment Moderation - time based bans ## Issue 6712 Comment Moderation - time based bans ## Approach - Consolidated the 3 types of blocking buttons in the comment content menu (i.e. Block, Moderator Block, Admin Block) into 1 regular Block button. - Show a modal when Block is clicked. - Let user choose the blocklist. - Let user choose the timeout duration (this PR's impetus). --- flow-typed/Comment.js | 38 +++- ui/comments.js | 2 +- ui/component/channelBlockButton/view.jsx | 4 +- ui/component/commentMenuList/index.js | 15 +- ui/component/commentMenuList/view.jsx | 67 +----- ui/constants/modal_types.js | 1 + ui/modal/modalBlockChannel/index.js | 25 +++ ui/modal/modalBlockChannel/view.jsx | 253 +++++++++++++++++++++++ ui/modal/modalRouter/view.jsx | 3 + ui/redux/actions/comments.js | 33 ++- ui/scss/component/_block-list.scss | 40 ++++ 11 files changed, 393 insertions(+), 88 deletions(-) create mode 100644 ui/modal/modalBlockChannel/index.js create mode 100644 ui/modal/modalBlockChannel/view.jsx diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js index ffda63876..2ac88e1f1 100644 --- a/flow-typed/Comment.js +++ b/flow-typed/Comment.js @@ -189,7 +189,43 @@ declare type SuperListResponse = { has_hidden_comments: boolean, }; -declare type ModerationBlockParams = {}; +declare type ModerationBlockParams = { + // Publisher, Moderator, or Commentron Admin + mod_channel_id: string, + mod_channel_name: string, + // Offender being blocked + blocked_channel_id: string, + blocked_channel_name: string, + // Creator that Moderator is delegated from. Used for delegated moderation + creator_channel_id?: string, + creator_channel_name?: string, + // Blocks identity from comment universally, requires Admin rights on commentron instance + block_all?: boolean, + time_out_hrs?: number, + // If true will delete all comments of the offender, requires Admin rights on commentron for universal delete + delete_all?: boolean, + // The usual signature stuff + signature: string, + signing_ts: string, +}; + +declare type ModerationBlockResponse = { + deleted_comment_ids: Array, + banned_channel_id: string, + all_blocked: boolean, + banned_from: string, +}; + +declare type BlockedListArgs = { + // Publisher, Moderator or Commentron Admin + mod_channel_id: string, + mod_channel_name: string, + // Creator that Moderator is delegated from. Used for delegated moderation + creator_channel_id?: string, + creator_channel_name?: string, + signature: string, + signing_ts: string, +}; declare type ModerationAddDelegateParams = { mod_channel_id: string, diff --git a/ui/comments.js b/ui/comments.js index 58593895f..315756401 100644 --- a/ui/comments.js +++ b/ui/comments.js @@ -14,7 +14,7 @@ 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_block_list: (params: BlockedListArgs) => fetchCommentsApi('moderation.BlockedList', params), moderation_add_delegate: (params: ModerationAddDelegateParams) => fetchCommentsApi('moderation.AddDelegate', params), moderation_remove_delegate: (params: ModerationRemoveDelegateParams) => fetchCommentsApi('moderation.RemoveDelegate', params), diff --git a/ui/component/channelBlockButton/view.jsx b/ui/component/channelBlockButton/view.jsx index dce36b899..7f10eeb35 100644 --- a/ui/component/channelBlockButton/view.jsx +++ b/ui/component/channelBlockButton/view.jsx @@ -12,7 +12,7 @@ type Props = { isBlockingOrUnBlocking: boolean, isToggling: boolean, doCommentModUnBlock: (string, boolean) => void, - doCommentModBlock: (string, boolean) => void, + doCommentModBlock: (string, ?Number, boolean) => void, doCommentModUnBlockAsAdmin: (string, string) => void, doCommentModBlockAsAdmin: (string, string) => void, doCommentModUnBlockAsModerator: (string, string, string) => void, @@ -42,7 +42,7 @@ function ChannelBlockButton(props: Props) { if (isBlocked) { doCommentModUnBlock(uri, false); } else { - doCommentModBlock(uri, false); + doCommentModBlock(uri, undefined, false); } break; diff --git a/ui/component/commentMenuList/index.js b/ui/component/commentMenuList/index.js index 5ebdf41b5..9d8c4ce48 100644 --- a/ui/component/commentMenuList/index.js +++ b/ui/component/commentMenuList/index.js @@ -1,19 +1,13 @@ import { connect } from 'react-redux'; import { makeSelectChannelPermUrlForClaimUri, makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux'; -import { - doCommentPin, - doCommentModBlock, - doCommentModBlockAsAdmin, - doCommentModBlockAsModerator, - doCommentModAddDelegate, -} from 'redux/actions/comments'; +import { doCommentPin, doCommentModAddDelegate } from 'redux/actions/comments'; import { doChannelMute } from 'redux/actions/blocked'; // import { doSetActiveChannel } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app'; import { doSetPlayingUri } from 'redux/actions/content'; import { selectActiveChannelClaim } from 'redux/selectors/app'; import { selectPlayingUri } from 'redux/selectors/content'; -import { selectModerationDelegatorsById } from 'redux/selectors/comments'; + import CommentMenuList from './view'; const select = (state, props) => ({ @@ -22,7 +16,6 @@ const select = (state, props) => ({ contentChannelPermanentUrl: makeSelectChannelPermUrlForClaimUri(props.uri)(state), activeChannelClaim: selectActiveChannelClaim(state), playingUri: selectPlayingUri(state), - moderationDelegatorsById: selectModerationDelegatorsById(state), }); const perform = (dispatch) => ({ @@ -31,10 +24,6 @@ const perform = (dispatch) => ({ muteChannel: (channelUri) => dispatch(doChannelMute(channelUri)), pinComment: (commentId, claimId, remove) => dispatch(doCommentPin(commentId, claimId, remove)), // setActiveChannel: channelId => dispatch(doSetActiveChannel(channelId)), - commentModBlock: (commenterUri) => dispatch(doCommentModBlock(commenterUri)), - commentModBlockAsAdmin: (commenterUri, blockerId) => dispatch(doCommentModBlockAsAdmin(commenterUri, blockerId)), - commentModBlockAsModerator: (commenterUri, creatorId, blockerId) => - dispatch(doCommentModBlockAsModerator(commenterUri, creatorId, blockerId)), commentModAddDelegate: (modChanId, modChanName, creatorChannelClaim) => dispatch(doCommentModAddDelegate(modChanId, modChanName, creatorChannelClaim, true)), }); diff --git a/ui/component/commentMenuList/view.jsx b/ui/component/commentMenuList/view.jsx index 0698652dd..d008c153d 100644 --- a/ui/component/commentMenuList/view.jsx +++ b/ui/component/commentMenuList/view.jsx @@ -8,6 +8,7 @@ import Icon from 'component/common/icon'; import { parseURI } from 'lbry-redux'; type Props = { + uri: ?string, authorUri: string, // full LBRY Channel URI: lbry://@channel#123... commentId: string, // sha256 digest identifying the comment isTopLevel: boolean, @@ -23,21 +24,18 @@ type Props = { contentChannelPermanentUrl: any, activeChannelClaim: ?ChannelClaim, playingUri: ?PlayingUri, - moderationDelegatorsById: { [string]: { global: boolean, delegators: { name: string, claimId: string } } }, // --- perform --- openModal: (id: string, {}) => void, clearPlayingUri: () => void, muteChannel: (string) => void, pinComment: (string, string, boolean) => Promise, - commentModBlock: (string) => void, - commentModBlockAsAdmin: (string, string) => void, - commentModBlockAsModerator: (string, string, string) => void, commentModAddDelegate: (string, string, ChannelClaim) => void, setQuickReply: (any) => void, }; function CommentMenuList(props: Props) { const { + uri, claim, authorUri, commentIsMine, @@ -50,35 +48,16 @@ function CommentMenuList(props: Props) { isTopLevel, isPinned, handleEditComment, - commentModBlock, - commentModBlockAsAdmin, - commentModBlockAsModerator, commentModAddDelegate, playingUri, disableEdit, disableRemove, - moderationDelegatorsById, openModal, supportAmount, setQuickReply, } = props; - const contentChannelClaim = !claim - ? null - : claim.value_type === 'channel' - ? claim - : claim.signing_channel && claim.is_channel_signature_valid - ? claim.signing_channel - : null; - - const activeModeratorInfo = activeChannelClaim && moderationDelegatorsById[activeChannelClaim.claim_id]; const activeChannelIsCreator = activeChannelClaim && activeChannelClaim.permanent_url === contentChannelPermanentUrl; - const activeChannelIsAdmin = activeChannelClaim && activeModeratorInfo && activeModeratorInfo.global; - const activeChannelIsModerator = - activeChannelClaim && - contentChannelClaim && - activeModeratorInfo && - Object.values(activeModeratorInfo.delegators).includes(contentChannelClaim.claim_id); function handlePinComment(commentId, claimId, remove) { pinComment(commentId, claimId, remove); @@ -98,7 +77,7 @@ function CommentMenuList(props: Props) { } function handleCommentBlock() { - commentModBlock(authorUri); + openModal(MODALS.BLOCK_CHANNEL, { contentUri: uri, commenterUri: authorUri }); } function handleCommentMute() { @@ -112,18 +91,6 @@ function CommentMenuList(props: Props) { } } - function blockCommentAsModerator() { - if (activeChannelClaim && contentChannelClaim) { - commentModBlockAsModerator(authorUri, contentChannelClaim.claim_id, activeChannelClaim.claim_id); - } - } - - function blockCommentAsAdmin() { - if (activeChannelClaim) { - commentModBlockAsAdmin(authorUri, activeChannelClaim.claim_id); - } - } - return ( {activeChannelIsCreator &&
{__('Creator tools')}
} @@ -197,34 +164,6 @@ function CommentMenuList(props: Props) { )} - {!commentIsMine && (activeChannelIsAdmin || activeChannelIsModerator) && ( -
{__('Moderator tools')}
- )} - - {!commentIsMine && activeChannelIsAdmin && ( - -
- - {__('Global Block')} -
- {__('Block this channel as global admin')} -
- )} - - {!commentIsMine && activeChannelIsModerator && ( - -
- - {__('Moderator Block')} -
- - {__('Block this channel on behalf of %creator%', { - creator: contentChannelClaim ? contentChannelClaim.name : __('creator'), - })} - -
- )} - {activeChannelClaim && (
diff --git a/ui/constants/modal_types.js b/ui/constants/modal_types.js index c43f04399..2355f3087 100644 --- a/ui/constants/modal_types.js +++ b/ui/constants/modal_types.js @@ -43,6 +43,7 @@ export const IMAGE_UPLOAD = 'image_upload'; export const MOBILE_SEARCH = 'mobile_search'; export const VIEW_IMAGE = 'view_image'; export const CONFIRM_REMOVE_BTC_SWAP_ADDRESS = 'confirm_remove_btc_swap_address'; +export const BLOCK_CHANNEL = 'block_channel'; export const COLLECTION_ADD = 'collection_add'; export const COLLECTION_DELETE = 'collection_delete'; export const CONFIRM_REMOVE_CARD = 'CONFIRM_REMOVE_CARD'; diff --git a/ui/modal/modalBlockChannel/index.js b/ui/modal/modalBlockChannel/index.js new file mode 100644 index 000000000..cf2fbc6df --- /dev/null +++ b/ui/modal/modalBlockChannel/index.js @@ -0,0 +1,25 @@ +import { connect } from 'react-redux'; +import { makeSelectClaimForUri } from 'lbry-redux'; +import { doHideModal } from 'redux/actions/app'; +import { doCommentModBlock, doCommentModBlockAsAdmin, doCommentModBlockAsModerator } from 'redux/actions/comments'; +import { selectActiveChannelClaim } from 'redux/selectors/app'; +import { selectModerationDelegatorsById } from 'redux/selectors/comments'; + +import ModalBlockChannel from './view'; + +const select = (state, props) => ({ + activeChannelClaim: selectActiveChannelClaim(state), + contentClaim: makeSelectClaimForUri(props.contentUri)(state), + moderationDelegatorsById: selectModerationDelegatorsById(state), +}); + +const perform = (dispatch) => ({ + closeModal: () => dispatch(doHideModal()), + commentModBlock: (commenterUri, timeoutHours) => dispatch(doCommentModBlock(commenterUri, timeoutHours)), + commentModBlockAsAdmin: (commenterUri, blockerId, timeoutHours) => + dispatch(doCommentModBlockAsAdmin(commenterUri, blockerId, timeoutHours)), + commentModBlockAsModerator: (commenterUri, creatorId, blockerId, timeoutHours) => + dispatch(doCommentModBlockAsModerator(commenterUri, creatorId, blockerId, timeoutHours)), +}); + +export default connect(select, perform)(ModalBlockChannel); diff --git a/ui/modal/modalBlockChannel/view.jsx b/ui/modal/modalBlockChannel/view.jsx new file mode 100644 index 000000000..50de3ac7b --- /dev/null +++ b/ui/modal/modalBlockChannel/view.jsx @@ -0,0 +1,253 @@ +// @flow +import React from 'react'; +import classnames from 'classnames'; +import Button from 'component/button'; +import ChannelThumbnail from 'component/channelThumbnail'; +import ClaimPreview from 'component/claimPreview'; +import Card from 'component/common/card'; +import { FormField } from 'component/common/form'; +import usePersistedState from 'effects/use-persisted-state'; +import { Modal } from 'modal/modal'; + +const TAB = { + PERSONAL: 'personal', + MODERATOR: 'moderator', + ADMIN: 'admin', +}; + +const BLOCK = { + PERMANENT: 'permanent', + TIMEOUT: 'timeout', +}; + +type Props = { + contentUri: string, + commenterUri: string, + // --- select --- + activeChannelClaim: ?ChannelClaim, + contentClaim: ?Claim, + moderationDelegatorsById: { [string]: { global: boolean, delegators: { name: string, claimId: string } } }, + // --- perform --- + closeModal: () => void, + commentModBlock: (string, ?number) => void, + commentModBlockAsAdmin: (string, string, ?number) => void, + commentModBlockAsModerator: (string, string, string, ?number) => void, +}; + +export default function ModalBlockChannel(props: Props) { + const { + commenterUri, + activeChannelClaim, + contentClaim, + moderationDelegatorsById, + closeModal, + commentModBlock, + commentModBlockAsAdmin, + commentModBlockAsModerator, + } = props; + + const contentChannelClaim = !contentClaim + ? null + : contentClaim.value_type === 'channel' + ? contentClaim + : contentClaim.signing_channel && contentClaim.is_channel_signature_valid + ? contentClaim.signing_channel + : null; + + const activeModeratorInfo = activeChannelClaim && moderationDelegatorsById[activeChannelClaim.claim_id]; + const activeChannelIsAdmin = activeChannelClaim && activeModeratorInfo && activeModeratorInfo.global; + const activeChannelIsModerator = + activeChannelClaim && + contentChannelClaim && + activeModeratorInfo && + Object.values(activeModeratorInfo.delegators).includes(contentChannelClaim.claim_id); + + const [tab, setTab] = usePersistedState('ModalBlockChannel:tab', TAB.PERSONAL); + const [blockType, setBlockType] = usePersistedState('ModalBlockChannel:blockType', BLOCK.PERMANENT); + const [timeoutHrs, setTimeoutHrs] = usePersistedState('ModalBlockChannel:timeoutHrs', 1); + const [timeoutHrsError, setTimeoutHrsError] = React.useState(''); + + const personalIsTheOnlyTab = !activeChannelIsModerator && !activeChannelIsAdmin; + const blockButtonDisabled = blockType === BLOCK.TIMEOUT && (timeoutHrs === 0 || !Number.isInteger(timeoutHrs)); + + // ************************************************************************** + // ************************************************************************** + + // Check 'tab' validity on mount. + React.useEffect(() => { + if ( + personalIsTheOnlyTab || + (tab === TAB.MODERATOR && !activeChannelIsModerator) || + (tab === TAB.ADMIN && !activeChannelIsAdmin) + ) { + setTab(TAB.PERSONAL); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + // 'timeoutHrs' sanity check. + React.useEffect(() => { + if (Number.isInteger(timeoutHrs) && timeoutHrs > 0) { + if (timeoutHrsError) { + setTimeoutHrsError(''); + } + } else { + if (!timeoutHrsError) { + setTimeoutHrsError('Invalid duration.'); + } + } + }, [timeoutHrs, timeoutHrsError]); + + // ************************************************************************** + // ************************************************************************** + + function getTabElem(value, label) { + return ( +
+ + + } + /> + + ); +} diff --git a/ui/modal/modalRouter/view.jsx b/ui/modal/modalRouter/view.jsx index 3f8e729f2..143dc6e11 100644 --- a/ui/modal/modalRouter/view.jsx +++ b/ui/modal/modalRouter/view.jsx @@ -8,6 +8,7 @@ import LoadingBarOneOff from 'component/loadingBarOneOff'; const ModalAffirmPurchase = lazyImport(() => import('modal/modalAffirmPurchase' /* webpackChunkName: "modalAffirmPurchase" */)); const ModalAutoGenerateThumbnail = lazyImport(() => import('modal/modalAutoGenerateThumbnail' /* webpackChunkName: "modalAutoGenerateThumbnail" */)); const ModalAutoUpdateDownloaded = lazyImport(() => import('modal/modalAutoUpdateDownloaded' /* webpackChunkName: "modalAutoUpdateDownloaded" */)); +const ModalBlockChannel = lazyImport(() => import('modal/modalBlockChannel' /* webpackChunkName: "modalBlockChannel" */)); const ModalClaimCollectionAdd = lazyImport(() => import('modal/modalClaimCollectionAdd' /* webpackChunkName: "modalClaimCollectionAdd" */)); const ModalCommentAcknowledgement = lazyImport(() => import('modal/modalCommentAcknowledgement' /* webpackChunkName: "modalCommentAcknowledgement" */)); const ModalConfirmAge = lazyImport(() => import('modal/modalConfirmAge' /* webpackChunkName: "modalConfirmAge" */)); @@ -149,6 +150,8 @@ function ModalRouter(props: Props) { return ModalMassTipsUnlock; case MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS: return ModalRemoveBtcSwapAddress; + case MODALS.BLOCK_CHANNEL: + return ModalBlockChannel; case MODALS.COLLECTION_ADD: return ModalClaimCollectionAdd; case MODALS.COLLECTION_DELETE: diff --git a/ui/redux/actions/comments.js b/ui/redux/actions/comments.js index a1ce7d175..272f6048a 100644 --- a/ui/redux/actions/comments.js +++ b/ui/redux/actions/comments.js @@ -738,6 +738,7 @@ function doCommentModToggleBlock( creatorId: string, blockerIds: Array, // [] = use all my channels blockLevel: string, + timeoutHours?: number, showLink: boolean = false ) { return async (dispatch: Dispatch, getState: GetState) => { @@ -844,6 +845,7 @@ function doCommentModToggleBlock( block_all: unblock ? undefined : blockLevel === BLOCK_LEVEL.ADMIN, global_un_block: unblock ? blockLevel === BLOCK_LEVEL.ADMIN : undefined, ...sharedModBlockParams, + time_out_hrs: unblock ? undefined : timeoutHours, }) ) ) @@ -920,12 +922,13 @@ function doCommentModToggleBlock( * Blocks the commenter for all channels that I own. * * @param commenterUri + * @param timeoutHours * @param showLink * @returns {function(Dispatch): *} */ -export function doCommentModBlock(commenterUri: string, showLink: boolean = true) { +export function doCommentModBlock(commenterUri: string, timeoutHours?: number, showLink: boolean = true) { return (dispatch: Dispatch) => { - return dispatch(doCommentModToggleBlock(false, commenterUri, '', [], BLOCK_LEVEL.SELF, showLink)); + return dispatch(doCommentModToggleBlock(false, commenterUri, '', [], BLOCK_LEVEL.SELF, timeoutHours, showLink)); }; } @@ -934,11 +937,14 @@ export function doCommentModBlock(commenterUri: string, showLink: boolean = true * * @param commenterUri * @param blockerId + * @param timeoutHours * @returns {function(Dispatch): *} */ -export function doCommentModBlockAsAdmin(commenterUri: string, blockerId: string) { +export function doCommentModBlockAsAdmin(commenterUri: string, blockerId: string, timeoutHours?: number) { return (dispatch: Dispatch) => { - return dispatch(doCommentModToggleBlock(false, commenterUri, '', blockerId ? [blockerId] : [], BLOCK_LEVEL.ADMIN)); + return dispatch( + doCommentModToggleBlock(false, commenterUri, '', blockerId ? [blockerId] : [], BLOCK_LEVEL.ADMIN, timeoutHours) + ); }; } @@ -949,12 +955,25 @@ export function doCommentModBlockAsAdmin(commenterUri: string, blockerId: string * @param commenterUri * @param creatorId * @param blockerId + * @param timeoutHours * @returns {function(Dispatch): *} */ -export function doCommentModBlockAsModerator(commenterUri: string, creatorId: string, blockerId: string) { +export function doCommentModBlockAsModerator( + commenterUri: string, + creatorId: string, + blockerId: string, + timeoutHours?: number +) { return (dispatch: Dispatch) => { return dispatch( - doCommentModToggleBlock(false, commenterUri, creatorId, blockerId ? [blockerId] : [], BLOCK_LEVEL.MODERATOR) + doCommentModToggleBlock( + false, + commenterUri, + creatorId, + blockerId ? [blockerId] : [], + BLOCK_LEVEL.MODERATOR, + timeoutHours + ) ); }; } @@ -968,7 +987,7 @@ export function doCommentModBlockAsModerator(commenterUri: string, creatorId: st */ export function doCommentModUnBlock(commenterUri: string, showLink: boolean = true) { return (dispatch: Dispatch) => { - return dispatch(doCommentModToggleBlock(true, commenterUri, '', [], BLOCK_LEVEL.SELF, showLink)); + return dispatch(doCommentModToggleBlock(true, commenterUri, '', [], BLOCK_LEVEL.SELF, undefined, showLink)); }; } diff --git a/ui/scss/component/_block-list.scss b/ui/scss/component/_block-list.scss index 4caf7d1a4..bf46aed68 100644 --- a/ui/scss/component/_block-list.scss +++ b/ui/scss/component/_block-list.scss @@ -16,3 +16,43 @@ margin-top: var(--spacing-xxs); } } + +.block-modal--values { + margin-left: var(--spacing-s); + + .help { + font-style: italic; + font-size: var(--font-xsmall); + } +} + +.block-modal--finalize { + margin-top: var(--spacing-l); +} + +.block-modal--active-channel { + padding: var(--spacing-xs); + display: flex; + align-items: center; + + .channel-thumbnail { + margin-right: var(--spacing-xs); + height: 1.8rem; + width: 1.8rem; + } + + @media (min-width: $breakpoint-small) { + border-left: 1px solid var(--color-border); + padding-left: var(--spacing-m); + margin-left: calc(var(--spacing-l) * 2); + } +} + +.block-modal--active-channel-label { + @extend .help; + font-size: var(--font-xxsmall); + margin-top: 0; + max-width: 10rem; + white-space: pre-line; + margin-right: var(--spacing-s); +} -- 2.45.3 From 663376e970d93d36c4023f9fa4e445323d1c3887 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Fri, 20 Aug 2021 09:04:48 +0800 Subject: [PATCH 2/7] Block timeout was changed from "hours" to "seconds" in Commentron --- flow-typed/Comment.js | 2 +- package.json | 1 + static/app-strings.json | 5 ++ ui/modal/modalBlockChannel/view.jsx | 81 ++++++++++++++++++++--------- ui/redux/actions/comments.js | 4 +- yarn.lock | 5 ++ 6 files changed, 71 insertions(+), 27 deletions(-) diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js index 2ac88e1f1..822810b26 100644 --- a/flow-typed/Comment.js +++ b/flow-typed/Comment.js @@ -201,7 +201,7 @@ declare type ModerationBlockParams = { creator_channel_name?: string, // Blocks identity from comment universally, requires Admin rights on commentron instance block_all?: boolean, - time_out_hrs?: number, + time_out?: number, // If true will delete all comments of the offender, requires Admin rights on commentron for universal delete delete_all?: boolean, // The usual signature stuff diff --git a/package.json b/package.json index e7303aa87..04c3ec4a8 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "electron-updater": "^4.2.4", "express": "^4.17.1", "if-env": "^1.0.4", + "parse-duration": "^1.0.0", "react-datetime-picker": "^3.2.1", "react-plastic": "^1.1.1", "react-top-loading-bar": "^2.0.1", diff --git a/static/app-strings.json b/static/app-strings.json index 9ce061707..4f9e45e9d 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -1744,6 +1744,11 @@ "Moderator Block": "Moderator Block", "Block this channel on behalf of %creator%": "Block this channel on behalf of %creator%", "creator": "creator", + "Enter the timeout duration. Examples: %examples%": "Enter the timeout duration. Examples: %examples%", + "Wow, banned for more than 100 years?": "Wow, banned for more than 100 years?", + "Invalid duration.": "Invalid duration.", + "Permanent": "Permanent", + "Timeout --[time-based ban instead of permanent]--": "Timeout", "Create a channel to change this setting.": "Create a channel to change this setting.", "Invalid channel URL \"%url%\"": "Invalid channel URL \"%url%\"", "Delegation": "Delegation", diff --git a/ui/modal/modalBlockChannel/view.jsx b/ui/modal/modalBlockChannel/view.jsx index 50de3ac7b..d15e7b9cf 100644 --- a/ui/modal/modalBlockChannel/view.jsx +++ b/ui/modal/modalBlockChannel/view.jsx @@ -1,11 +1,14 @@ // @flow import React from 'react'; import classnames from 'classnames'; +import parseDuration from 'parse-duration'; import Button from 'component/button'; import ChannelThumbnail from 'component/channelThumbnail'; import ClaimPreview from 'component/claimPreview'; import Card from 'component/common/card'; import { FormField } from 'component/common/form'; +import Icon from 'component/common/icon'; +import * as ICONS from 'constants/icons'; import usePersistedState from 'effects/use-persisted-state'; import { Modal } from 'modal/modal'; @@ -64,11 +67,12 @@ export default function ModalBlockChannel(props: Props) { const [tab, setTab] = usePersistedState('ModalBlockChannel:tab', TAB.PERSONAL); const [blockType, setBlockType] = usePersistedState('ModalBlockChannel:blockType', BLOCK.PERMANENT); - const [timeoutHrs, setTimeoutHrs] = usePersistedState('ModalBlockChannel:timeoutHrs', 1); - const [timeoutHrsError, setTimeoutHrsError] = React.useState(''); + const [timeoutInput, setTimeoutInput] = usePersistedState('ModalBlockChannel:timeoutInput', '10m'); + const [timeoutInputErr, setTimeoutInputErr] = React.useState(''); + const [timeoutSec, setTimeoutSec] = React.useState(-1); const personalIsTheOnlyTab = !activeChannelIsModerator && !activeChannelIsAdmin; - const blockButtonDisabled = blockType === BLOCK.TIMEOUT && (timeoutHrs === 0 || !Number.isInteger(timeoutHrs)); + const blockButtonDisabled = blockType === BLOCK.TIMEOUT && timeoutSec < 1; // ************************************************************************** // ************************************************************************** @@ -84,18 +88,39 @@ export default function ModalBlockChannel(props: Props) { } }, []); // eslint-disable-line react-hooks/exhaustive-deps - // 'timeoutHrs' sanity check. + // 'timeoutInput' to 'timeoutSec' conversion. React.useEffect(() => { - if (Number.isInteger(timeoutHrs) && timeoutHrs > 0) { - if (timeoutHrsError) { - setTimeoutHrsError(''); + const setInvalid = (errMsg: string) => { + if (timeoutSec !== -1) { + setTimeoutSec(-1); + } + if (!timeoutInputErr) { + setTimeoutInputErr(errMsg); + } + }; + + const setValid = (seconds) => { + if (seconds !== timeoutSec) { + setTimeoutSec(seconds); + } + if (timeoutInputErr) { + setTimeoutInputErr(''); + } + }; + + const ONE_HUNDRED_YEARS_IN_SECONDS = 3154000000; + const seconds = parseDuration(timeoutInput, 's'); + + if (Number.isInteger(seconds) && seconds > 0) { + if (seconds > ONE_HUNDRED_YEARS_IN_SECONDS) { + setInvalid('Wow, banned for more than 100 years?'); + } else { + setValid(seconds); } } else { - if (!timeoutHrsError) { - setTimeoutHrsError('Invalid duration.'); - } + setInvalid('Invalid duration.'); } - }, [timeoutHrs, timeoutHrsError]); + }, [timeoutInput, timeoutInputErr, timeoutSec]); // ************************************************************************** // ************************************************************************** @@ -143,19 +168,27 @@ export default function ModalBlockChannel(props: Props) { } function getTimeoutDurationElem() { + const examples = '\n- 30s\n- 10m\n- 1h\n- 2d\n- 3mo\n- 1y'; return ( setTimeoutHrs(parseInt(e.target.value))} - error={timeoutHrsError} + name="time_out" + label={ + <> + {__('Duration')} + + + } + type="text" + placeholder="30s, 10m, 1h, 2d, 3mo, 1y" + value={timeoutInput} + onChange={(e) => setTimeoutInput(e.target.value)} + error={timeoutInputErr} /> ); } @@ -180,7 +213,7 @@ export default function ModalBlockChannel(props: Props) { } function handleBlock() { - const duration = blockType === BLOCK.TIMEOUT && timeoutHrs ? timeoutHrs : undefined; + const duration = blockType === BLOCK.TIMEOUT && timeoutSec > 0 ? timeoutSec : undefined; switch (tab) { case TAB.PERSONAL: @@ -232,7 +265,7 @@ export default function ModalBlockChannel(props: Props) {
{getBlockTypeElem(BLOCK.PERMANENT, 'Permanent')} - {getBlockTypeElem(BLOCK.TIMEOUT, 'Timeout')} + {getBlockTypeElem(BLOCK.TIMEOUT, 'Timeout --[time-based ban instead of permanent]--')}
{blockType === BLOCK.TIMEOUT && getTimeoutDurationElem()}
diff --git a/ui/redux/actions/comments.js b/ui/redux/actions/comments.js index 272f6048a..dd97f8301 100644 --- a/ui/redux/actions/comments.js +++ b/ui/redux/actions/comments.js @@ -738,7 +738,7 @@ function doCommentModToggleBlock( creatorId: string, blockerIds: Array, // [] = use all my channels blockLevel: string, - timeoutHours?: number, + timeoutSec?: number, showLink: boolean = false ) { return async (dispatch: Dispatch, getState: GetState) => { @@ -845,7 +845,7 @@ function doCommentModToggleBlock( block_all: unblock ? undefined : blockLevel === BLOCK_LEVEL.ADMIN, global_un_block: unblock ? blockLevel === BLOCK_LEVEL.ADMIN : undefined, ...sharedModBlockParams, - time_out_hrs: unblock ? undefined : timeoutHours, + time_out: unblock ? undefined : timeoutSec, }) ) ) diff --git a/yarn.lock b/yarn.lock index 2b521ec43..9b1e9da25 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11916,6 +11916,11 @@ parse-asn1@^5.0.0: pbkdf2 "^3.0.3" safe-buffer "^5.1.1" +parse-duration@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-duration/-/parse-duration-1.0.0.tgz#8605651745f61088f6fb14045c887526c291858c" + integrity sha512-X4kUkCTHU1N/kEbwK9FpUJ0UZQa90VzeczfS704frR30gljxDG0pSziws06XlK+CGRSo/1wtG1mFIdBFQTMQNw== + parse-entities@^1.0.2, parse-entities@^1.1.0: version "1.2.2" resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50" -- 2.45.3 From 0c1554e453ff7054be34a879f3bc34f19515b313 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Fri, 20 Aug 2021 15:18:54 +0800 Subject: [PATCH 3/7] Blocklist Page: show the timeout ban duration - 'humanize-duration' is used because 'moment''s humanizer sucks. --- flow-typed/Comment.js | 3 +++ package.json | 1 + static/app-strings.json | 1 + ui/page/listBlocked/index.js | 6 ++++++ ui/page/listBlocked/view.jsx | 39 +++++++++++++++++++++++++++++++++- ui/redux/actions/comments.js | 31 +++++++++++++++++++++++---- ui/redux/reducers/comments.js | 18 ++++++++++++++-- ui/redux/selectors/comments.js | 4 ++++ yarn.lock | 5 +++++ 9 files changed, 101 insertions(+), 7 deletions(-) diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js index 822810b26..f62993f9e 100644 --- a/flow-typed/Comment.js +++ b/flow-typed/Comment.js @@ -60,6 +60,9 @@ declare type CommentsState = { fetchingModerationDelegators: boolean, blockingByUri: {}, unBlockingByUri: {}, + personalTimeoutMap: { [uri: string]: { blockedAt: string, bannedFor: number, banRemaining: number } }, + adminTimeoutMap: { [uri: string]: { blockedAt: string, bannedFor: number, banRemaining: number } }, + moderatorTimeoutMap: { [uri: string]: { blockedAt: string, bannedFor: number, banRemaining: number } }, togglingForDelegatorMap: {[string]: Array}, // {"blockedUri": ["delegatorUri1", ""delegatorUri2", ...]} settingsByChannelId: { [string]: PerChannelSettings }, // ChannelID -> settings fetchingSettings: boolean, diff --git a/package.json b/package.json index 04c3ec4a8..f9caeed7a 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "electron-notarize": "^1.0.0", "electron-updater": "^4.2.4", "express": "^4.17.1", + "humanize-duration": "^3.27.0", "if-env": "^1.0.4", "parse-duration": "^1.0.0", "react-datetime-picker": "^3.2.1", diff --git a/static/app-strings.json b/static/app-strings.json index 4f9e45e9d..381022242 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -1749,6 +1749,7 @@ "Invalid duration.": "Invalid duration.", "Permanent": "Permanent", "Timeout --[time-based ban instead of permanent]--": "Timeout", + "(Remaining: %duration%) --[timeout ban duration]--": "(Remaining: %duration%)", "Create a channel to change this setting.": "Create a channel to change this setting.", "Invalid channel URL \"%url%\"": "Invalid channel URL \"%url%\"", "Delegation": "Delegation", diff --git a/ui/page/listBlocked/index.js b/ui/page/listBlocked/index.js index e3e1d5c55..178b11aa9 100644 --- a/ui/page/listBlocked/index.js +++ b/ui/page/listBlocked/index.js @@ -8,6 +8,9 @@ import { selectModeratorBlockListDelegatorsMap, selectFetchingModerationBlockList, selectModerationDelegatorsById, + selectAdminTimeoutMap, + selectModeratorTimeoutMap, + selectPersonalTimeoutMap, } from 'redux/selectors/comments'; import { selectMyChannelClaims } from 'lbry-redux'; import ListBlocked from './view'; @@ -17,6 +20,9 @@ const select = (state) => ({ personalBlockList: selectModerationBlockList(state), adminBlockList: selectAdminBlockList(state), moderatorBlockList: selectModeratorBlockList(state), + personalTimeoutMap: selectPersonalTimeoutMap(state), + adminTimeoutMap: selectAdminTimeoutMap(state), + moderatorTimeoutMap: selectModeratorTimeoutMap(state), moderatorBlockListDelegatorsMap: selectModeratorBlockListDelegatorsMap(state), delegatorsById: selectModerationDelegatorsById(state), myChannelClaims: selectMyChannelClaims(state), diff --git a/ui/page/listBlocked/view.jsx b/ui/page/listBlocked/view.jsx index 85f355947..8036a8f1f 100644 --- a/ui/page/listBlocked/view.jsx +++ b/ui/page/listBlocked/view.jsx @@ -3,6 +3,8 @@ import * as ICONS from 'constants/icons'; import { BLOCK_LEVEL } from 'constants/comment'; import React from 'react'; import classnames from 'classnames'; +import moment from 'moment'; +import humanizeDuration from 'humanize-duration'; import ClaimList from 'component/claimList'; import ClaimPreview from 'component/claimPreview'; import Page from 'component/page'; @@ -25,6 +27,9 @@ type Props = { personalBlockList: ?Array, adminBlockList: ?Array, moderatorBlockList: ?Array, + personalTimeoutMap: { [uri: string]: { blockedAt: string, bannedFor: number, banRemaining: number } }, + adminTimeoutMap: { [uri: string]: { blockedAt: string, bannedFor: number, banRemaining: number } }, + moderatorTimeoutMap: { [uri: string]: { blockedAt: string, bannedFor: number, banRemaining: number } }, moderatorBlockListDelegatorsMap: { [string]: Array }, fetchingModerationBlockList: boolean, fetchModBlockedList: () => void, @@ -39,6 +44,9 @@ function ListBlocked(props: Props) { personalBlockList, adminBlockList, moderatorBlockList, + personalTimeoutMap, + adminTimeoutMap, + moderatorTimeoutMap, moderatorBlockListDelegatorsMap, fetchingModerationBlockList, fetchModBlockedList, @@ -97,17 +105,45 @@ function ListBlocked(props: Props) { } function getButtons(view, uri) { + const getDurationStr = (durationNs) => { + const NANO_TO_MS = 1000000; + return humanizeDuration(durationNs / NANO_TO_MS, { round: true }); + }; + + const getBanInfoElem = (timeoutInfo) => { + return ( +
+
+
+ {moment(timeoutInfo.blockedAt).format('MMMM Do, YYYY @ HH:mm')} +
+ {getDurationStr(timeoutInfo.bannedFor)}{' '} + {__('(Remaining: %duration%) --[timeout ban duration]--', { + duration: getDurationStr(timeoutInfo.banRemaining), + })} +
+
+
+ ); + }; + switch (view) { case VIEW.BLOCKED: return ( <> + {personalTimeoutMap[uri] && getBanInfoElem(personalTimeoutMap[uri])} ); case VIEW.ADMIN: - return ; + return ( + <> + + {adminTimeoutMap[uri] && getBanInfoElem(adminTimeoutMap[uri])} + + ); case VIEW.MODERATOR: const delegatorUrisForBlockedUri = localModeratorListDelegatorsMap && localModeratorListDelegatorsMap[uri]; @@ -121,6 +157,7 @@ function ListBlocked(props: Props) { + {moderatorTimeoutMap[uri] && getBanInfoElem(moderatorTimeoutMap[uri])} ); })} diff --git a/ui/redux/actions/comments.js b/ui/redux/actions/comments.js index dd97f8301..dc85ba754 100644 --- a/ui/redux/actions/comments.js +++ b/ui/redux/actions/comments.js @@ -1054,13 +1054,20 @@ export function doFetchModBlockedList() { let moderatorBlockList = []; let moderatorBlockListDelegatorsMap = {}; + // These should just be part of the block list above, but it is + // separated for now because there are too many clients that we need + // to update. + const personalTimeoutMap = {}; + const adminTimeoutMap = {}; + const moderatorTimeoutMap = {}; + const blockListsPerChannel = res.map((r) => r.value); blockListsPerChannel .sort((a, b) => { return 1; }) .forEach((channelBlockLists) => { - const storeList = (fetchedList, blockedList, blockedByMap) => { + const storeList = (fetchedList, blockedList, timeoutMap, blockedByMap) => { if (fetchedList) { fetchedList.forEach((blockedChannel) => { if (blockedChannel.blocked_channel_name) { @@ -1071,6 +1078,14 @@ export function doFetchModBlockedList() { if (!blockedList.find((blockedChannel) => isURIEqual(blockedChannel.channelUri, channelUri))) { blockedList.push({ channelUri, blockedAt: blockedChannel.blocked_at }); + + if (blockedChannel.banned_for) { + timeoutMap[channelUri] = { + blockedAt: blockedChannel.blocked_at, + bannedFor: blockedChannel.banned_for, + banRemaining: blockedChannel.ban_remaining, + }; + } } if (blockedByMap !== undefined) { @@ -1096,9 +1111,14 @@ export function doFetchModBlockedList() { 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); + storeList(blocked_channels, personalBlockList, personalTimeoutMap); + storeList(globally_blocked_channels, adminBlockList, adminTimeoutMap); + storeList( + delegated_blocked_channels, + moderatorBlockList, + moderatorTimeoutMap, + moderatorBlockListDelegatorsMap + ); }); dispatch({ @@ -1123,6 +1143,9 @@ export function doFetchModBlockedList() { .map((blockedChannel) => blockedChannel.channelUri) : null, moderatorBlockListDelegatorsMap: moderatorBlockListDelegatorsMap, + personalTimeoutMap, + adminTimeoutMap, + moderatorTimeoutMap, }, }); }) diff --git a/ui/redux/reducers/comments.js b/ui/redux/reducers/comments.js index 815025698..04c0bed8d 100644 --- a/ui/redux/reducers/comments.js +++ b/ui/redux/reducers/comments.js @@ -40,6 +40,9 @@ const defaultState: CommentsState = { fetchingModerationDelegators: false, blockingByUri: {}, unBlockingByUri: {}, + personalTimeoutMap: {}, + adminTimeoutMap: {}, + moderatorTimeoutMap: {}, togglingForDelegatorMap: {}, settingsByChannelId: {}, // ChannelId -> PerChannelSettings fetchingSettings: false, @@ -671,14 +674,25 @@ export default handleActions( fetchingModerationBlockList: true, }), [ACTIONS.COMMENT_MODERATION_BLOCK_LIST_COMPLETED]: (state: CommentsState, action: any) => { - const { personalBlockList, adminBlockList, moderatorBlockList, moderatorBlockListDelegatorsMap } = action.data; + const { + personalBlockList, + adminBlockList, + moderatorBlockList, + moderatorBlockListDelegatorsMap, + personalTimeoutMap, + adminTimeoutMap, + moderatorTimeoutMap, + } = action.data; return { ...state, moderationBlockList: personalBlockList, adminBlockList: adminBlockList, moderatorBlockList: moderatorBlockList, - moderatorBlockListDelegatorsMap: moderatorBlockListDelegatorsMap, + moderatorBlockListDelegatorsMap, + personalTimeoutMap, + adminTimeoutMap, + moderatorTimeoutMap, fetchingModerationBlockList: false, }; }, diff --git a/ui/redux/selectors/comments.js b/ui/redux/selectors/comments.js index e4c21ff54..bc930d9cd 100644 --- a/ui/redux/selectors/comments.js +++ b/ui/redux/selectors/comments.js @@ -45,6 +45,10 @@ export const selectModeratorBlockList = createSelector(selectState, (state) => state.moderatorBlockList ? state.moderatorBlockList.reverse() : [] ); +export const selectPersonalTimeoutMap = createSelector(selectState, (state) => state.personalTimeoutMap); +export const selectAdminTimeoutMap = createSelector(selectState, (state) => state.adminTimeoutMap); +export const selectModeratorTimeoutMap = createSelector(selectState, (state) => state.moderatorTimeoutMap); + export const selectModeratorBlockListDelegatorsMap = createSelector( selectState, (state) => state.moderatorBlockListDelegatorsMap diff --git a/yarn.lock b/yarn.lock index 9b1e9da25..c65da973f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8558,6 +8558,11 @@ https-proxy-agent@^4.0.0: agent-base "5" debug "4" +humanize-duration@^3.27.0: + version "3.27.0" + resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.27.0.tgz#3f781b7cf8022ad587f76b9839b60bc2b29636b2" + integrity sha512-qLo/08cNc3Tb0uD7jK0jAcU5cnqCM0n568918E7R2XhMr/+7F37p4EY062W/stg7tmzvknNn9b/1+UhVRzsYrQ== + humanize-plus@^1.8.1: version "1.8.2" resolved "https://registry.yarnpkg.com/humanize-plus/-/humanize-plus-1.8.2.tgz#a65b34459ad6367adbb3707a82a3c9f916167030" -- 2.45.3 From e8df055063d4518aa8d0e1d0fdee97d30a124516 Mon Sep 17 00:00:00 2001 From: zeppi Date: Fri, 3 Sep 2021 10:25:14 -0400 Subject: [PATCH 4/7] do not set stale recsys id --- ui/recsys.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/recsys.js b/ui/recsys.js index f5d8ae106..6a92e2bd2 100644 --- a/ui/recsys.js +++ b/ui/recsys.js @@ -110,7 +110,7 @@ const recsys = { uid: userId, // selectUser claimId: claimId, pageLoadedAt: Date.now(), - recsysId: makeSelectRecommendedRecsysIdForClaimId(claimId)(state) || recsysId, + recsysId: null, recClaimIds: [], recClickedVideoIdx: [], events: [], -- 2.45.3 From 34232b3dc802a597341033bfcbf067a12d7218fe Mon Sep 17 00:00:00 2001 From: zeppi Date: Fri, 3 Sep 2021 10:29:20 -0400 Subject: [PATCH 5/7] no watchman send if videoplay not initialized --- ui/analytics.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/analytics.js b/ui/analytics.js index b36902941..495535dcc 100644 --- a/ui/analytics.js +++ b/ui/analytics.js @@ -120,6 +120,10 @@ async function sendAndResetWatchmanData() { return 'Can only be used with a user id'; } + if (!videoPlayer) { + return 'Video player not initialized'; + } + let timeSinceLastIntervalSend = new Date() - lastSentTime; lastSentTime = new Date(); -- 2.45.3 From 804edd3308ce495b0552bc1ed7fee804aab78a15 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Fri, 3 Sep 2021 16:47:06 +0800 Subject: [PATCH 6/7] Restrict "Timeout" feature to content owners The main reason to do this is because we are doing extra comment filtering using our blocklist (so that we don't see people we blocked regardless on who's content we are at), but we are also using a cached blocklist so we don't know when a Timeout will be lifted to allow new livestream messages. We will need Commentron Issue-80 to truly fix this. For the case of when we are the owner of the content, we don't run the extra filtering (since it is equivalent to Commentron's filtering), hence the issue doesn't exist. Any new livestream messages received through websocket won't be locally filtered. --- ui/modal/modalBlockChannel/view.jsx | 32 ++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/ui/modal/modalBlockChannel/view.jsx b/ui/modal/modalBlockChannel/view.jsx index d15e7b9cf..454f1a370 100644 --- a/ui/modal/modalBlockChannel/view.jsx +++ b/ui/modal/modalBlockChannel/view.jsx @@ -71,21 +71,26 @@ export default function ModalBlockChannel(props: Props) { const [timeoutInputErr, setTimeoutInputErr] = React.useState(''); const [timeoutSec, setTimeoutSec] = React.useState(-1); - const personalIsTheOnlyTab = !activeChannelIsModerator && !activeChannelIsAdmin; + const isPersonalTheOnlyTab = !activeChannelIsModerator && !activeChannelIsAdmin; + const isTimeoutAvail = contentClaim && contentClaim.is_my_output; const blockButtonDisabled = blockType === BLOCK.TIMEOUT && timeoutSec < 1; // ************************************************************************** // ************************************************************************** - // Check 'tab' validity on mount. + // Check settings validity on mount. React.useEffect(() => { if ( - personalIsTheOnlyTab || + isPersonalTheOnlyTab || (tab === TAB.MODERATOR && !activeChannelIsModerator) || (tab === TAB.ADMIN && !activeChannelIsAdmin) ) { setTab(TAB.PERSONAL); } + + if (!isTimeoutAvail && blockType === BLOCK.TIMEOUT) { + setBlockType(BLOCK.PERMANENT); + } }, []); // eslint-disable-line react-hooks/exhaustive-deps // 'timeoutInput' to 'timeoutSec' conversion. @@ -154,13 +159,14 @@ export default function ModalBlockChannel(props: Props) { } } - function getBlockTypeElem(value, label) { + function getBlockTypeElem(value, label, disabled = false, disabledLabel = '') { return ( setBlockType(value)} /> @@ -239,6 +245,13 @@ export default function ModalBlockChannel(props: Props) { // ************************************************************************** // ************************************************************************** + if (isPersonalTheOnlyTab && !isTimeoutAvail) { + // There's only 1 option. Just execute it and don't show the modal. + commentModBlock(commenterUri); + closeModal(); + return null; + } + return ( - {!personalIsTheOnlyTab && ( + {!isPersonalTheOnlyTab && (
@@ -265,7 +278,12 @@ export default function ModalBlockChannel(props: Props) {
{getBlockTypeElem(BLOCK.PERMANENT, 'Permanent')} - {getBlockTypeElem(BLOCK.TIMEOUT, 'Timeout --[time-based ban instead of permanent]--')} + {getBlockTypeElem( + BLOCK.TIMEOUT, + 'Timeout --[time-based ban instead of permanent]--', + !isTimeoutAvail, + 'Timeout (only available on content that you own)' + )}
{blockType === BLOCK.TIMEOUT && getTimeoutDurationElem()}
-- 2.45.3 From bf8ab2e9ce38c7e2fe7168e956e67123f39842ab Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Fri, 3 Sep 2021 15:43:01 +0800 Subject: [PATCH 7/7] Run the extra app-side comment filter only if the claim is not ours. ## Preamble - The app-side uses a cached blocklist, so when a Timeout expires, it doesn't know that the ban has been lifted. - Meanwhile, we are doing extra comment filtering using this blocklist (so that we don't see comments that we have blocked, regardless of whose claim we are viewing). ## Issue In a livestream, if a new message from an ex-offender comes in after their ban has been lifted, we do get the websocket message but it's being filtered out locally as mentioned above. So, the msg ended up being visible for everyone except the owner. ## Fix (band aid) - Don't run the extra filter if the claim we are viewing is ours -- commentron would have filtered it for us anyways, and is the right logic to use even before this Timeout feature is introduced. - For the case of Timeout, this only serves as a band-aid until Commentron Issue 80 is available for us to detect the ban has been lifted. This is because it doesn't handle the case where I am a viewer and I decided to timeout someone for a few minutes. Because I am not the owner of the claim, the offender will continue to be blocked due to the same issue mentioned above. --- ui/redux/selectors/comments.js | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/ui/redux/selectors/comments.js b/ui/redux/selectors/comments.js index bc930d9cd..82365b9fa 100644 --- a/ui/redux/selectors/comments.js +++ b/ui/redux/selectors/comments.js @@ -214,7 +214,7 @@ export const makeSelectCommentsForUri = (uri: string) => (state, byClaimId, byUri) => { const claimId = byUri[uri]; const comments = byClaimId && byClaimId[claimId]; - return makeSelectFilteredComments(comments)(state); + return makeSelectFilteredComments(comments, claimId)(state); } ); @@ -226,7 +226,7 @@ export const makeSelectTopLevelCommentsForUri = (uri: string) => (state, byClaimId, byUri) => { const claimId = byUri[uri]; const comments = byClaimId && byClaimId[claimId]; - return makeSelectFilteredComments(comments)(state); + return makeSelectFilteredComments(comments, claimId)(state); } ); @@ -262,7 +262,13 @@ export const makeSelectRepliesForParentId = (id: string) => } ); -const makeSelectFilteredComments = (comments: Array) => +/** + * makeSelectFilteredComments + * + * @param comments List of comments to filter. + * @param claimId The claim that `comments` reside in. + */ +const makeSelectFilteredComments = (comments: Array, claimId?: string) => createSelector( selectClaimsById, selectMyActiveClaims, @@ -315,12 +321,18 @@ const makeSelectFilteredComments = (comments: Array) => } } - return !( - mutedChannels.includes(comment.channel_url) || - personalBlockList.includes(comment.channel_url) || - adminBlockList.includes(comment.channel_url) || - moderatorBlockList.includes(comment.channel_url) - ); + if (claimId) { + const claimIdIsMine = myClaims && myClaims.size > 0 && myClaims.has(claimId); + if (!claimIdIsMine) { + return !( + personalBlockList.includes(comment.channel_url) || + adminBlockList.includes(comment.channel_url) || + moderatorBlockList.includes(comment.channel_url) + ); + } + } + + return !mutedChannels.includes(comment.channel_url); }) : []; } -- 2.45.3