diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js index 17d8eac53..551699e3e 100644 --- a/flow-typed/Comment.js +++ b/flow-typed/Comment.js @@ -58,7 +58,6 @@ declare type CommentsState = { blockingByUri: {}, unBlockingByUri: {}, togglingForDelegatorMap: {[string]: Array}, // {"blockedUri": ["delegatorUri1", ""delegatorUri2", ...]} - commentsDisabledChannelIds: Array, settingsByChannelId: { [string]: PerChannelSettings }, // ChannelID -> settings fetchingSettings: boolean, fetchingBlockedWords: boolean, @@ -222,10 +221,20 @@ declare type ModerationAmIParams = { }; declare type SettingsParams = { - channel_name: string, + channel_name?: string, channel_id: string, - signature: string, - signing_ts: string, + signature?: string, + signing_ts?: string, +}; + +declare type SettingsResponse = { + words?: string, + comments_enabled: boolean, + min_tip_amount_comment: number, + min_tip_amount_super_chat: number, + slow_mode_min_gap: number, + curse_jar_amount: number, + filters_enabled?: boolean, }; declare type UpdateSettingsParams = { diff --git a/ui/comments.js b/ui/comments.js index 9069e49d3..58593895f 100644 --- a/ui/comments.js +++ b/ui/comments.js @@ -34,6 +34,7 @@ const Comments = { setting_unblock_word: (params: BlockWordParams) => fetchCommentsApi('setting.UnBlockWord', params), setting_list_blocked_words: (params: SettingsParams) => fetchCommentsApi('setting.ListBlockedWords', params), setting_update: (params: UpdateSettingsParams) => fetchCommentsApi('setting.Update', params), + setting_get: (params: SettingsParams) => fetchCommentsApi('setting.Get', params), super_list: (params: SuperListParams) => fetchCommentsApi('comment.SuperChatList', params), }; diff --git a/ui/component/commentCreate/index.js b/ui/component/commentCreate/index.js index a02de664f..66bb84f01 100644 --- a/ui/component/commentCreate/index.js +++ b/ui/component/commentCreate/index.js @@ -7,30 +7,42 @@ import { doSendTip, } from 'lbry-redux'; import { doOpenModal, doSetActiveChannel } from 'redux/actions/app'; -import { doCommentCreate } from 'redux/actions/comments'; +import { doCommentCreate, doFetchCreatorSettings } from 'redux/actions/comments'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectActiveChannelClaim } from 'redux/selectors/app'; -import { makeSelectCommentsDisabledForUri } from 'redux/selectors/comments'; +import { selectSettingsByChannelId } from 'redux/selectors/comments'; import { CommentCreate } from './view'; import { doToast } from 'redux/actions/notifications'; const select = (state, props) => ({ commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true, - commentsDisabledBySettings: makeSelectCommentsDisabledForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state), channels: selectMyChannelClaims(state), isFetchingChannels: selectFetchingMyChannels(state), activeChannelClaim: selectActiveChannelClaim(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state), + settingsByChannelId: selectSettingsByChannelId(state), }); const perform = (dispatch, ownProps) => ({ createComment: (comment, claimId, parentId, txid, payment_intent_id, environment) => - dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, ownProps.livestream, txid, payment_intent_id, environment)), + dispatch( + doCommentCreate( + comment, + claimId, + parentId, + ownProps.uri, + ownProps.livestream, + txid, + payment_intent_id, + environment + ) + ), openModal: (modal, props) => dispatch(doOpenModal(modal, props)), setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)), sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)), doToast: (options) => dispatch(doToast(options)), + doFetchCreatorSettings: (channelClaimId) => dispatch(doFetchCreatorSettings(channelClaimId)), }); export default connect(select, perform)(CommentCreate); diff --git a/ui/component/commentCreate/view.jsx b/ui/component/commentCreate/view.jsx index 238187cb4..b6f02353f 100644 --- a/ui/component/commentCreate/view.jsx +++ b/ui/component/commentCreate/view.jsx @@ -6,6 +6,7 @@ import * as ICONS from 'constants/icons'; import React from 'react'; import classnames from 'classnames'; import { FormField, Form } from 'component/common/form'; +import Icon from 'component/common/icon'; import Button from 'component/button'; import SelectChannel from 'component/selectChannel'; import usePersistedState from 'effects/use-persisted-state'; @@ -14,8 +15,10 @@ import { useHistory } from 'react-router'; import WalletTipAmountSelector from 'component/walletTipAmountSelector'; import CreditAmount from 'component/common/credit-amount'; import ChannelThumbnail from 'component/channelThumbnail'; +import I18nMessage from 'component/i18nMessage'; import UriIndicator from 'component/uriIndicator'; import Empty from 'component/common/empty'; +import { getChannelIdFromClaim } from 'util/claim'; import { Lbryio } from 'lbryinc'; let stripeEnvironment = 'test'; @@ -32,7 +35,6 @@ type Props = { uri: string, claim: StreamClaim, createComment: (string, string, string, ?string, ?string, ?string) => Promise, - commentsDisabledBySettings: boolean, channels: ?Array, onDoneReplying?: () => void, onCancelReplying?: () => void, @@ -50,12 +52,13 @@ type Props = { sendTip: ({}, (any) => void, (any) => void) => void, doToast: ({ message: string }) => void, disabled: boolean, + doFetchCreatorSettings: (channelId: string) => Promise, + settingsByChannelId: { [channelId: string]: PerChannelSettings }, }; export function CommentCreate(props: Props) { const { createComment, - commentsDisabledBySettings, claim, channels, onDoneReplying, @@ -71,6 +74,8 @@ export function CommentCreate(props: Props) { claimIsMine, sendTip, doToast, + doFetchCreatorSettings, + settingsByChannelId, } = props; const buttonRef: ElementRef = React.useRef(); const { @@ -92,6 +97,40 @@ export function CommentCreate(props: Props) { const [tipError, setTipError] = React.useState(); const disabled = isSubmitting || !activeChannelClaim || !commentValue.length; const [shouldDisableReviewButton, setShouldDisableReviewButton] = React.useState(); + const channelId = getChannelIdFromClaim(claim); + const channelSettings = channelId ? settingsByChannelId[channelId] : undefined; + const minSuper = (channelSettings && channelSettings.min_tip_amount_super_chat) || 0; + const minTip = (channelSettings && channelSettings.min_tip_amount_comment) || 0; + const minAmount = minTip || minSuper || 0; + const minAmountMet = minAmount === 0 || tipAmount >= minAmount; + + const minAmountRef = React.useRef(minAmount); + minAmountRef.current = minAmount; + + const MinAmountNotice = minAmount ? ( +
+ }}> + {minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''} + + +
+ ) : null; + + // ************************************************************************** + // Functions + // ************************************************************************** function handleCommentChange(event) { let commentValue; @@ -131,6 +170,14 @@ export function CommentCreate(props: Props) { return; } + if (!channelId) { + doToast({ + message: __('Unable to verify channel settings. Try refreshing the page.'), + isError: true, + }); + return; + } + // if comment post didn't work, but tip was already made, try again to create comment if (commentFailure && tipAmount === successTip.tipAmount) { handleCreateComment(successTip.txid); @@ -139,6 +186,29 @@ export function CommentCreate(props: Props) { setSuccessTip({ txid: undefined, tipAmount: undefined }); } + // !! Beware of stale closure when editing the then-block, including doSubmitTip(). + doFetchCreatorSettings(channelId).then(() => { + const lockedMinAmount = minAmount; // value during closure. + const currentMinAmount = minAmountRef.current; // value from latest doFetchCreatorSettings(). + + if (lockedMinAmount !== currentMinAmount) { + doToast({ + message: __('The creator just updated the minimum setting. Please revise or double-check your tip amount.'), + isError: true, + }); + setIsReviewingSupportComment(false); + return; + } + + doSubmitTip(); + }); + } + + function doSubmitTip() { + if (!activeChannelClaim) { + return; + } + const params = { amount: tipAmount, claim_id: claimId, @@ -268,6 +338,12 @@ export function CommentCreate(props: Props) { .catch(() => { setIsSubmitting(false); setCommentFailure(true); + + if (channelId) { + // It could be that the creator added a minimum tip setting. + // Manually update for now until a websocket msg is available. + doFetchCreatorSettings(channelId); + } }); } @@ -275,7 +351,22 @@ export function CommentCreate(props: Props) { setAdvancedEditor(!advancedEditor); } - if (commentsDisabledBySettings) { + // ************************************************************************** + // Effects + // ************************************************************************** + + // Fetch channel constraints if not already. + React.useEffect(() => { + if (!channelSettings && channelId) { + doFetchCreatorSettings(channelId); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + // ************************************************************************** + // Render + // ************************************************************************** + + if (channelSettings && !channelSettings.comments_enabled) { return ; } @@ -331,7 +422,7 @@ export function CommentCreate(props: Props) {