Creator: Enable "min tips" and "min hyperchat" #6824

Merged
infinite-persistence merged 6 commits from ip/min.tips into master 2021-08-13 04:41:02 +02:00
13 changed files with 426 additions and 272 deletions

17
flow-typed/Comment.js vendored
View file

@ -58,7 +58,6 @@ declare type CommentsState = {
blockingByUri: {}, blockingByUri: {},
unBlockingByUri: {}, unBlockingByUri: {},
togglingForDelegatorMap: {[string]: Array<string>}, // {"blockedUri": ["delegatorUri1", ""delegatorUri2", ...]} togglingForDelegatorMap: {[string]: Array<string>}, // {"blockedUri": ["delegatorUri1", ""delegatorUri2", ...]}
commentsDisabledChannelIds: Array<string>,
settingsByChannelId: { [string]: PerChannelSettings }, // ChannelID -> settings settingsByChannelId: { [string]: PerChannelSettings }, // ChannelID -> settings
fetchingSettings: boolean, fetchingSettings: boolean,
fetchingBlockedWords: boolean, fetchingBlockedWords: boolean,
@ -222,10 +221,20 @@ declare type ModerationAmIParams = {
}; };
declare type SettingsParams = { declare type SettingsParams = {
channel_name: string, channel_name?: string,
channel_id: string, channel_id: string,
signature: string, signature?: string,
signing_ts: 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 = { declare type UpdateSettingsParams = {

View file

@ -34,6 +34,7 @@ const Comments = {
setting_unblock_word: (params: BlockWordParams) => fetchCommentsApi('setting.UnBlockWord', params), setting_unblock_word: (params: BlockWordParams) => fetchCommentsApi('setting.UnBlockWord', params),
setting_list_blocked_words: (params: SettingsParams) => fetchCommentsApi('setting.ListBlockedWords', params), setting_list_blocked_words: (params: SettingsParams) => fetchCommentsApi('setting.ListBlockedWords', params),
setting_update: (params: UpdateSettingsParams) => fetchCommentsApi('setting.Update', 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), super_list: (params: SuperListParams) => fetchCommentsApi('comment.SuperChatList', params),
}; };

View file

@ -7,30 +7,42 @@ import {
doSendTip, doSendTip,
} from 'lbry-redux'; } from 'lbry-redux';
import { doOpenModal, doSetActiveChannel } from 'redux/actions/app'; 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 { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectActiveChannelClaim } from 'redux/selectors/app'; import { selectActiveChannelClaim } from 'redux/selectors/app';
import { makeSelectCommentsDisabledForUri } from 'redux/selectors/comments'; import { selectSettingsByChannelId } from 'redux/selectors/comments';
import { CommentCreate } from './view'; import { CommentCreate } from './view';
import { doToast } from 'redux/actions/notifications'; import { doToast } from 'redux/actions/notifications';
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),
activeChannelClaim: selectActiveChannelClaim(state), activeChannelClaim: selectActiveChannelClaim(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
settingsByChannelId: selectSettingsByChannelId(state),
}); });
const perform = (dispatch, ownProps) => ({ const perform = (dispatch, ownProps) => ({
createComment: (comment, claimId, parentId, txid, payment_intent_id, environment) => 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)), openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)), setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)),
sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)), sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)),
doToast: (options) => dispatch(doToast(options)), doToast: (options) => dispatch(doToast(options)),
doFetchCreatorSettings: (channelClaimId) => dispatch(doFetchCreatorSettings(channelClaimId)),
}); });
export default connect(select, perform)(CommentCreate); export default connect(select, perform)(CommentCreate);

View file

@ -6,6 +6,7 @@ import * as ICONS from 'constants/icons';
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { FormField, Form } from 'component/common/form'; import { FormField, Form } from 'component/common/form';
import Icon from 'component/common/icon';
import Button from 'component/button'; import Button from 'component/button';
import SelectChannel from 'component/selectChannel'; import SelectChannel from 'component/selectChannel';
import usePersistedState from 'effects/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
@ -14,8 +15,10 @@ import { useHistory } from 'react-router';
import WalletTipAmountSelector from 'component/walletTipAmountSelector'; 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 I18nMessage from 'component/i18nMessage';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
import Empty from 'component/common/empty'; import Empty from 'component/common/empty';
import { getChannelIdFromClaim } from 'util/claim';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
let stripeEnvironment = 'test'; let stripeEnvironment = 'test';
@ -32,7 +35,6 @@ type Props = {
uri: string, uri: string,
claim: StreamClaim, claim: StreamClaim,
createComment: (string, string, string, ?string, ?string, ?string) => Promise<any>, createComment: (string, string, string, ?string, ?string, ?string) => Promise<any>,
commentsDisabledBySettings: boolean,
channels: ?Array<ChannelClaim>, channels: ?Array<ChannelClaim>,
onDoneReplying?: () => void, onDoneReplying?: () => void,
onCancelReplying?: () => void, onCancelReplying?: () => void,
@ -50,12 +52,13 @@ type Props = {
sendTip: ({}, (any) => void, (any) => void) => void, sendTip: ({}, (any) => void, (any) => void) => void,
doToast: ({ message: string }) => void, doToast: ({ message: string }) => void,
disabled: boolean, disabled: boolean,
doFetchCreatorSettings: (channelId: string) => Promise<any>,
settingsByChannelId: { [channelId: string]: PerChannelSettings },
}; };
export function CommentCreate(props: Props) { export function CommentCreate(props: Props) {
const { const {
createComment, createComment,
commentsDisabledBySettings,
claim, claim,
channels, channels,
onDoneReplying, onDoneReplying,
@ -71,6 +74,8 @@ export function CommentCreate(props: Props) {
claimIsMine, claimIsMine,
sendTip, sendTip,
doToast, doToast,
doFetchCreatorSettings,
settingsByChannelId,
} = props; } = props;
const buttonRef: ElementRef<any> = React.useRef(); const buttonRef: ElementRef<any> = React.useRef();
const { const {
@ -92,6 +97,40 @@ export function CommentCreate(props: Props) {
const [tipError, setTipError] = React.useState(); const [tipError, setTipError] = React.useState();
const disabled = isSubmitting || !activeChannelClaim || !commentValue.length; const disabled = isSubmitting || !activeChannelClaim || !commentValue.length;
const [shouldDisableReviewButton, setShouldDisableReviewButton] = React.useState(); 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 ? (
<div className="help--notice comment--min-amount-notice">
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
</I18nMessage>
<Icon
customTooltipText={
minTip
? __('This channel requires a minimum tip for each comment.')
: minSuper
? __('This channel requires a minimum amount for HyperChats to be visible.')
: ''
}
className="icon--help"
icon={ICONS.HELP}
tooltip
size={16}
/>
</div>
) : null;
// **************************************************************************
// Functions
// **************************************************************************
function handleCommentChange(event) { function handleCommentChange(event) {
let commentValue; let commentValue;
@ -131,6 +170,14 @@ export function CommentCreate(props: Props) {
return; 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 comment post didn't work, but tip was already made, try again to create comment
if (commentFailure && tipAmount === successTip.tipAmount) { if (commentFailure && tipAmount === successTip.tipAmount) {
handleCreateComment(successTip.txid); handleCreateComment(successTip.txid);
@ -139,6 +186,29 @@ export function CommentCreate(props: Props) {
setSuccessTip({ txid: undefined, tipAmount: undefined }); 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 = { const params = {
amount: tipAmount, amount: tipAmount,
claim_id: claimId, claim_id: claimId,
@ -268,6 +338,12 @@ export function CommentCreate(props: Props) {
.catch(() => { .catch(() => {
setIsSubmitting(false); setIsSubmitting(false);
setCommentFailure(true); 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); 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 <Empty padded text={__('This channel has disabled comments on their page.')} />; return <Empty padded text={__('This channel has disabled comments on their page.')} />;
} }
@ -331,7 +422,7 @@ export function CommentCreate(props: Props) {
<Button <Button
autoFocus autoFocus
button="primary" button="primary"
disabled={disabled} disabled={disabled || !minAmountMet}
label={ label={
isSubmitting isSubmitting
? __('Sending...') ? __('Sending...')
@ -347,6 +438,7 @@ export function CommentCreate(props: Props) {
label={__('Cancel')} label={__('Cancel')}
onClick={() => setIsReviewingSupportComment(false)} onClick={() => setIsReviewingSupportComment(false)}
/> />
{MinAmountNotice}
</div> </div>
</div> </div>
); );
@ -400,7 +492,7 @@ export function CommentCreate(props: Props) {
{isSupportComment ? ( {isSupportComment ? (
<> <>
<Button <Button
disabled={disabled || tipError || shouldDisableReviewButton} disabled={disabled || tipError || shouldDisableReviewButton || !minAmountMet}
type="button" type="button"
button="primary" button="primary"
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE} icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
@ -412,22 +504,24 @@ export function CommentCreate(props: Props) {
</> </>
) : ( ) : (
<> <>
<Button {(!minTip || claimIsMine) && (
ref={buttonRef} <Button
button="primary" ref={buttonRef}
disabled={disabled} button="primary"
type="submit" disabled={disabled}
label={ type="submit"
isReply label={
? isSubmitting isReply
? __('Replying...') ? isSubmitting
: __('Reply') ? __('Replying...')
: isSubmitting : __('Reply')
? __('Commenting...') : isSubmitting
: __('Comment --[button to submit something]--') ? __('Commenting...')
} : __('Comment --[button to submit something]--')
requiresAuth={IS_WEB} }
/> requiresAuth={IS_WEB}
/>
)}
{!claimIsMine && ( {!claimIsMine && (
<Button <Button
disabled={disabled} disabled={disabled}
@ -452,7 +546,7 @@ export function CommentCreate(props: Props) {
}} }}
/> />
)} )}
{isReply && ( {isReply && !minTip && (
<Button <Button
button="link" button="link"
label={__('Cancel')} label={__('Cancel')}
@ -465,6 +559,7 @@ export function CommentCreate(props: Props) {
)} )}
</> </>
)} )}
{MinAmountNotice}
</div> </div>
</Form> </Form>
); );

View file

@ -1,5 +1,10 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectClaimIsMine, selectFetchingMyChannels, selectMyChannelClaims } from 'lbry-redux'; import {
makeSelectClaimForUri,
makeSelectClaimIsMine,
selectFetchingMyChannels,
selectMyChannelClaims,
} from 'lbry-redux';
import { import {
makeSelectTopLevelCommentsForUri, makeSelectTopLevelCommentsForUri,
makeSelectTopLevelTotalPagesForUri, makeSelectTopLevelTotalPagesForUri,
@ -7,12 +12,11 @@ import {
selectIsFetchingReacts, selectIsFetchingReacts,
makeSelectTotalCommentsCountForUri, makeSelectTotalCommentsCountForUri,
selectOthersReactsById, selectOthersReactsById,
makeSelectCommentsDisabledForUri,
selectMyReactionsByCommentId, selectMyReactionsByCommentId,
makeSelectCommentIdsForUri, makeSelectCommentIdsForUri,
selectSettingsByChannelId,
} from 'redux/selectors/comments'; } from 'redux/selectors/comments';
import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments'; import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectActiveChannelClaim } from 'redux/selectors/app'; import { selectActiveChannelClaim } from 'redux/selectors/app';
import CommentsList from './view'; import CommentsList from './view';
@ -24,12 +28,12 @@ const select = (state, props) => {
topLevelComments: makeSelectTopLevelCommentsForUri(props.uri)(state), topLevelComments: makeSelectTopLevelCommentsForUri(props.uri)(state),
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(props.uri)(state), topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(props.uri)(state),
totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state), totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
isFetchingComments: selectIsFetchingComments(state), isFetchingComments: selectIsFetchingComments(state),
isFetchingReacts: selectIsFetchingReacts(state), isFetchingReacts: selectIsFetchingReacts(state),
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
commentsDisabledBySettings: makeSelectCommentsDisabledForUri(props.uri)(state),
fetchingChannels: selectFetchingMyChannels(state), fetchingChannels: selectFetchingMyChannels(state),
settingsByChannelId: selectSettingsByChannelId(state),
myReactsByCommentId: selectMyReactionsByCommentId(state), myReactsByCommentId: selectMyReactionsByCommentId(state),
othersReactsById: selectOthersReactsById(state), othersReactsById: selectOthersReactsById(state),
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id, activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,

View file

@ -14,6 +14,7 @@ import { ENABLE_COMMENT_REACTIONS } from 'config';
import Empty from 'component/common/empty'; import Empty from 'component/common/empty';
import debounce from 'util/debounce'; import debounce from 'util/debounce';
import { useIsMobile } from 'effects/use-screensize'; import { useIsMobile } from 'effects/use-screensize';
import { getChannelIdFromClaim } from 'util/claim';
const DEBOUNCE_SCROLL_HANDLER_MS = 200; const DEBOUNCE_SCROLL_HANDLER_MS = 200;
@ -29,12 +30,12 @@ type Props = {
allCommentIds: any, allCommentIds: any,
topLevelComments: Array<Comment>, topLevelComments: Array<Comment>,
topLevelTotalPages: number, topLevelTotalPages: number,
commentsDisabledBySettings: boolean,
fetchTopLevelComments: (string, number, number, number) => void, fetchTopLevelComments: (string, number, number, number) => void,
fetchComment: (string) => void, fetchComment: (string) => void,
fetchReacts: (Array<string>) => Promise<any>, fetchReacts: (Array<string>) => Promise<any>,
resetComments: (string) => void, resetComments: (string) => void,
uri: string, uri: string,
claim: ?Claim,
claimIsMine: boolean, claimIsMine: boolean,
myChannels: ?Array<ChannelClaim>, myChannels: ?Array<ChannelClaim>,
isFetchingComments: boolean, isFetchingComments: boolean,
@ -45,6 +46,7 @@ type Props = {
myReactsByCommentId: ?{ [string]: Array<string> }, // "CommentId:MyChannelId" -> reaction array (note the ID concatenation) myReactsByCommentId: ?{ [string]: Array<string> }, // "CommentId:MyChannelId" -> reaction array (note the ID concatenation)
othersReactsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } }, othersReactsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } },
activeChannelId: ?string, activeChannelId: ?string,
settingsByChannelId: { [channelId: string]: PerChannelSettings },
}; };
function CommentList(props: Props) { function CommentList(props: Props) {
@ -57,7 +59,7 @@ function CommentList(props: Props) {
uri, uri,
topLevelComments, topLevelComments,
topLevelTotalPages, topLevelTotalPages,
commentsDisabledBySettings, claim,
claimIsMine, claimIsMine,
myChannels, myChannels,
isFetchingComments, isFetchingComments,
@ -68,6 +70,7 @@ function CommentList(props: Props) {
myReactsByCommentId, myReactsByCommentId,
othersReactsById, othersReactsById,
activeChannelId, activeChannelId,
settingsByChannelId,
} = props; } = props;
const commentRef = React.useRef(); const commentRef = React.useRef();
@ -78,6 +81,8 @@ function CommentList(props: Props) {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [expandedComments, setExpandedComments] = React.useState(!isMobile); const [expandedComments, setExpandedComments] = React.useState(!isMobile);
const totalFetchedComments = allCommentIds ? allCommentIds.length : 0; const totalFetchedComments = allCommentIds ? allCommentIds.length : 0;
const channelId = getChannelIdFromClaim(claim);
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
// Display comments immediately if not fetching reactions // Display comments immediately if not fetching reactions
// If not, wait to show comments until reactions are fetched // If not, wait to show comments until reactions are fetched
@ -279,7 +284,7 @@ function CommentList(props: Props) {
<> <>
<CommentCreate uri={uri} /> <CommentCreate uri={uri} />
{!commentsDisabledBySettings && !isFetchingComments && hasNoComments && ( {channelSettings && channelSettings.comments_enabled && !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

@ -29,7 +29,7 @@ const select = (state) => ({
const perform = (dispatch) => ({ const perform = (dispatch) => ({
commentBlockWords: (channelClaim, words) => dispatch(doCommentBlockWords(channelClaim, words)), commentBlockWords: (channelClaim, words) => dispatch(doCommentBlockWords(channelClaim, words)),
commentUnblockWords: (channelClaim, words) => dispatch(doCommentUnblockWords(channelClaim, words)), commentUnblockWords: (channelClaim, words) => dispatch(doCommentUnblockWords(channelClaim, words)),
fetchCreatorSettings: (channelClaimIds) => dispatch(doFetchCreatorSettings(channelClaimIds)), fetchCreatorSettings: (channelClaimId) => dispatch(doFetchCreatorSettings(channelClaimId)),
updateCreatorSettings: (channelClaim, settings) => dispatch(doUpdateCreatorSettings(channelClaim, settings)), updateCreatorSettings: (channelClaim, settings) => dispatch(doUpdateCreatorSettings(channelClaim, settings)),
commentModAddDelegate: (modChanId, modChanName, creatorChannelClaim) => commentModAddDelegate: (modChanId, modChanName, creatorChannelClaim) =>
dispatch(doCommentModAddDelegate(modChanId, modChanName, creatorChannelClaim)), dispatch(doCommentModAddDelegate(modChanId, modChanName, creatorChannelClaim)),

View file

@ -11,11 +11,17 @@ import LbcSymbol from 'component/common/lbc-symbol';
import I18nMessage from 'component/i18nMessage'; import I18nMessage from 'component/i18nMessage';
import { isNameValid, parseURI } from 'lbry-redux'; import { isNameValid, parseURI } from 'lbry-redux';
import ClaimPreview from 'component/claimPreview'; import ClaimPreview from 'component/claimPreview';
import debounce from 'util/debounce';
import { getUriForSearchTerm } from 'util/search'; import { getUriForSearchTerm } from 'util/search';
const DEBOUNCE_REFRESH_MS = 1000; const DEBOUNCE_REFRESH_MS = 1000;
const FEATURE_IS_READY = false; const LBC_MAX = 21000000;
const LBC_MIN = 0;
const LBC_STEP = 1.0;
// ****************************************************************************
// ****************************************************************************
type Props = { type Props = {
activeChannelClaim: ChannelClaim, activeChannelClaim: ChannelClaim,
@ -28,7 +34,7 @@ type Props = {
commentModAddDelegate: (string, string, ChannelClaim) => void, commentModAddDelegate: (string, string, ChannelClaim) => void,
commentModRemoveDelegate: (string, string, ChannelClaim) => void, commentModRemoveDelegate: (string, string, ChannelClaim) => void,
commentModListDelegates: (ChannelClaim) => void, commentModListDelegates: (ChannelClaim) => void,
fetchCreatorSettings: (Array<string>) => void, fetchCreatorSettings: (channelId: string) => void,
updateCreatorSettings: (ChannelClaim, PerChannelSettings) => void, updateCreatorSettings: (ChannelClaim, PerChannelSettings) => void,
doToast: ({ message: string }) => void, doToast: ({ message: string }) => void,
}; };
@ -54,26 +60,28 @@ export default function SettingsCreatorPage(props: Props) {
const [moderatorSearchTerm, setModeratorSearchTerm] = React.useState(''); const [moderatorSearchTerm, setModeratorSearchTerm] = React.useState('');
const [moderatorSearchError, setModeratorSearchError] = React.useState(''); const [moderatorSearchError, setModeratorSearchError] = React.useState('');
const [moderatorSearchClaimUri, setModeratorSearchClaimUri] = React.useState(''); const [moderatorSearchClaimUri, setModeratorSearchClaimUri] = React.useState('');
const [minTipAmountComment, setMinTipAmountComment] = React.useState(0); const [minTip, setMinTip] = React.useState(0);
const [minTipAmountSuperChat, setMinTipAmountSuperChat] = React.useState(0); const [minSuper, setMinSuper] = React.useState(0);
const [slowModeMinGap, setSlowModeMinGap] = React.useState(0); const [slowModeMin, setSlowModeMin] = React.useState(0);
const [lastUpdated, setLastUpdated] = React.useState(1); const [lastUpdated, setLastUpdated] = React.useState(1);
function settingsToStates(settings: PerChannelSettings) { const pushSlowModeMinDebounced = React.useMemo(() => debounce(pushSlowModeMin, 1000), []);
if (settings.comments_enabled !== undefined) { const pushMinTipDebounced = React.useMemo(() => debounce(pushMinTip, 1000), []);
setCommentsEnabled(settings.comments_enabled); const pushMinSuperDebounced = React.useMemo(() => debounce(pushMinSuper, 1000), []);
}
if (settings.min_tip_amount_comment !== undefined) { // **************************************************************************
setMinTipAmountComment(settings.min_tip_amount_comment); // **************************************************************************
}
if (settings.min_tip_amount_super_chat !== undefined) { /**
setMinTipAmountSuperChat(settings.min_tip_amount_super_chat); * Updates corresponding GUI states with the given PerChannelSettings values.
} *
if (settings.slow_mode_min_gap !== undefined) { * @param settings
setSlowModeMinGap(settings.slow_mode_min_gap); * @param fullSync If true, update all states and consider 'undefined' settings as "cleared/false";
} * if false, only update defined settings.
if (settings.words) { */
const tagArray = Array.from(new Set(settings.words)); function settingsToStates(settings: PerChannelSettings, fullSync: boolean) {
const doSetMutedWordTags = (words: Array<string>) => {
const tagArray = Array.from(new Set(words));
setMutedWordTags( setMutedWordTags(
tagArray tagArray
.filter((t) => t !== '') .filter((t) => t !== '')
@ -81,15 +89,51 @@ export default function SettingsCreatorPage(props: Props) {
return { name: x }; return { name: x };
}) })
); );
};
if (fullSync) {
setCommentsEnabled(settings.comments_enabled || false);
setMinTip(settings.min_tip_amount_comment || 0);
setMinSuper(settings.min_tip_amount_super_chat || 0);
setSlowModeMin(settings.slow_mode_min_gap || 0);
doSetMutedWordTags(settings.words || []);
} else {
if (settings.comments_enabled !== undefined) {
setCommentsEnabled(settings.comments_enabled);
}
if (settings.min_tip_amount_comment !== undefined) {
setMinTip(settings.min_tip_amount_comment);
}
if (settings.min_tip_amount_super_chat !== undefined) {
setMinSuper(settings.min_tip_amount_super_chat);
}
if (settings.slow_mode_min_gap !== undefined) {
setSlowModeMin(settings.slow_mode_min_gap);
}
if (settings.words) {
doSetMutedWordTags(settings.words);
}
} }
} }
function setSettings(newSettings: PerChannelSettings) { function setSettings(newSettings: PerChannelSettings) {
settingsToStates(newSettings); settingsToStates(newSettings, false);
updateCreatorSettings(activeChannelClaim, newSettings); updateCreatorSettings(activeChannelClaim, newSettings);
setLastUpdated(Date.now()); setLastUpdated(Date.now());
} }
function pushSlowModeMin(value: number, activeChannelClaim: ChannelClaim) {
updateCreatorSettings(activeChannelClaim, { slow_mode_min_gap: value });
}
function pushMinTip(value: number, activeChannelClaim: ChannelClaim) {
updateCreatorSettings(activeChannelClaim, { min_tip_amount_comment: value });
}
function pushMinSuper(value: number, activeChannelClaim: ChannelClaim) {
updateCreatorSettings(activeChannelClaim, { min_tip_amount_super_chat: value });
}
function addMutedWords(newTags: Array<Tag>) { function addMutedWords(newTags: Array<Tag>) {
const validatedNewTags = []; const validatedNewTags = [];
newTags.forEach((newTag) => { newTags.forEach((newTag) => {
@ -153,6 +197,9 @@ export default function SettingsCreatorPage(props: Props) {
} }
} }
// **************************************************************************
// **************************************************************************
// 'moderatorSearchTerm' to 'moderatorSearchClaimUri' // 'moderatorSearchTerm' to 'moderatorSearchClaimUri'
React.useEffect(() => { React.useEffect(() => {
if (!moderatorSearchTerm) { if (!moderatorSearchTerm) {
@ -211,20 +258,23 @@ export default function SettingsCreatorPage(props: Props) {
if (activeChannelClaim && settingsByChannelId && settingsByChannelId[activeChannelClaim.claim_id]) { if (activeChannelClaim && settingsByChannelId && settingsByChannelId[activeChannelClaim.claim_id]) {
const channelSettings = settingsByChannelId[activeChannelClaim.claim_id]; const channelSettings = settingsByChannelId[activeChannelClaim.claim_id];
settingsToStates(channelSettings); settingsToStates(channelSettings, true);
} }
}, [activeChannelClaim, settingsByChannelId, lastUpdated]); }, [activeChannelClaim, settingsByChannelId, lastUpdated]);
// Re-sync list, mainly to correct any invalid settings. // Re-sync list on first idle time; mainly to correct any invalid settings.
React.useEffect(() => { React.useEffect(() => {
if (lastUpdated && activeChannelClaim) { if (lastUpdated && activeChannelClaim) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
fetchCreatorSettings([activeChannelClaim.claim_id]); fetchCreatorSettings(activeChannelClaim.claim_id);
}, DEBOUNCE_REFRESH_MS); }, DEBOUNCE_REFRESH_MS);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [lastUpdated, activeChannelClaim, fetchCreatorSettings]); }, [lastUpdated, activeChannelClaim, fetchCreatorSettings]);
// **************************************************************************
// **************************************************************************
const isBusy = const isBusy =
!activeChannelClaim || !settingsByChannelId || settingsByChannelId[activeChannelClaim.claim_id] === undefined; !activeChannelClaim || !settingsByChannelId || settingsByChannelId[activeChannelClaim.claim_id] === undefined;
const isDisabled = const isDisabled =
@ -272,8 +322,13 @@ export default function SettingsCreatorPage(props: Props) {
step={1} step={1}
type="number" type="number"
placeholder="1" placeholder="1"
value={slowModeMinGap} value={slowModeMin}
onChange={(e) => setSettings({ slow_mode_min_gap: parseInt(e.target.value) })} onChange={(e) => {
const value = parseInt(e.target.value);
setSlowModeMin(value);
pushSlowModeMinDebounced(value, activeChannelClaim);
}}
onBlur={() => setLastUpdated(Date.now())}
/> />
</> </>
} }
@ -297,47 +352,70 @@ export default function SettingsCreatorPage(props: Props) {
</div> </div>
} }
/> />
{FEATURE_IS_READY && ( <Card
<Card title={__('Tip')}
title={__('Tip')} actions={
actions={ <>
<> <FormField
<FormField name="min_tip_amount_comment"
name="min_tip_amount_comment" label={
label={ <I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for comments</I18nMessage>
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for comments</I18nMessage> }
helper={__(
'Enabling a minimum amount to comment will force all comments, including livestreams, to have tips associated with them. This can help prevent spam.'
)}
className="form-field--price-amount"
max={LBC_MAX}
min={LBC_MIN}
step={LBC_STEP}
type="number"
placeholder="1"
value={minTip}
onChange={(e) => {
const newMinTip = parseFloat(e.target.value);
setMinTip(newMinTip);
pushMinTipDebounced(newMinTip, activeChannelClaim);
if (newMinTip !== 0 && minSuper !== 0) {
setMinSuper(0);
pushMinSuperDebounced(0, activeChannelClaim);
} }
helper={__( }}
'Enabling a minimum amount to comment will force all comments, including livestreams, to have tips associated with them. This can help prevent spam.' onBlur={() => setLastUpdated(Date.now())}
)} />
className="form-field--price-amount" <FormField
min={0} name="min_tip_amount_super_chat"
step="any" label={
type="number" <I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for hyperchats</I18nMessage>
placeholder="1" }
value={minTipAmountComment} helper={
onChange={(e) => setSettings({ min_tip_amount_comment: parseFloat(e.target.value) })} <>
/> {__(
<FormField 'Enabling a minimum amount to hyperchat will force all TIPPED comments to have this value in order to be shown. This still allows regular comments to be posted.'
name="min_tip_amount_super_chat" )}
label={ {minTip !== 0 && (
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for hyperchats</I18nMessage> <p className="help--inline">
} <em>{__('(This settings is not applicable if all comments require a tip.)')}</em>
helper={__( </p>
'Enabling a minimum amount to hyperchat will force all TIPPED comments to have this value in order to be shown. This still allows regular comments to be posted.' )}
)} </>
className="form-field--price-amount" }
min={0} className="form-field--price-amount"
step="any" min={0}
type="number" step="any"
placeholder="1" type="number"
value={minTipAmountSuperChat} placeholder="1"
onChange={(e) => setSettings({ min_tip_amount_super_chat: parseFloat(e.target.value) })} value={minSuper}
/> disabled={minTip !== 0}
</> onChange={(e) => {
} const newMinSuper = parseFloat(e.target.value);
/> setMinSuper(newMinSuper);
)} pushMinSuperDebounced(newMinSuper, activeChannelClaim);
}}
onBlur={() => setLastUpdated(Date.now())}
/>
</>
}
/>
<Card <Card
title={__('Delegation')} title={__('Delegation')}
className="card--enable-overflow" className="card--enable-overflow"

View file

@ -104,7 +104,7 @@ export function doCommentList(
totalFilteredItems: total_filtered_items, totalFilteredItems: total_filtered_items,
totalPages: total_pages, totalPages: total_pages,
claimId: claimId, claimId: claimId,
authorClaimId: authorChannelClaim ? authorChannelClaim.claim_id : undefined, commenterClaimId: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
uri: uri, uri: uri,
}, },
}); });
@ -1357,7 +1357,7 @@ export function doFetchCommentModAmIList(channelClaim: ChannelClaim) {
}; };
} }
export const doFetchCreatorSettings = (channelClaimIds: Array<string> = []) => { export const doFetchCreatorSettings = (channelId: string) => {
return async (dispatch: Dispatch, getState: GetState) => { return async (dispatch: Dispatch, getState: GetState) => {
const state = getState(); const state = getState();
const myChannels = selectMyChannelClaims(state); const myChannels = selectMyChannelClaims(state);
@ -1366,84 +1366,49 @@ export const doFetchCreatorSettings = (channelClaimIds: Array<string> = []) => {
type: ACTIONS.COMMENT_FETCH_SETTINGS_STARTED, type: ACTIONS.COMMENT_FETCH_SETTINGS_STARTED,
}); });
let channelSignatures = []; let signedName;
if (myChannels) { if (myChannels) {
for (const channelClaim of myChannels) { const index = myChannels.findIndex((myChannel) => myChannel.claim_id === channelId);
if (channelClaimIds.length !== 0 && !channelClaimIds.includes(channelClaim.claim_id)) { if (index > -1) {
continue; signedName = await channelSignName(channelId, myChannels[index].name);
}
try {
const channelSignature = await Lbry.channel_sign({
channel_id: channelClaim.claim_id,
hexdata: toHex(channelClaim.name),
});
channelSignatures.push({ ...channelSignature, claim_id: channelClaim.claim_id, name: channelClaim.name });
} catch (e) {}
} }
} }
return Promise.all( const cmd = signedName ? Comments.setting_list : Comments.setting_get;
channelSignatures.map((signatureData) =>
Comments.setting_list({
channel_name: signatureData.name,
channel_id: signatureData.claim_id,
signature: signatureData.signature,
signing_ts: signatureData.signing_ts,
})
)
)
.then((settings) => {
const settingsByChannelId = {};
for (let i = 0; i < channelSignatures.length; ++i) {
const channelId = channelSignatures[i].claim_id;
settingsByChannelId[channelId] = settings[i];
if (settings[i].words) {
settingsByChannelId[channelId].words = settings[i].words.split(',');
}
delete settingsByChannelId[channelId].channel_name;
delete settingsByChannelId[channelId].channel_id;
delete settingsByChannelId[channelId].signature;
delete settingsByChannelId[channelId].signing_ts;
}
return cmd({
channel_id: channelId,
channel_name: (signedName && signedName.name) || undefined,
signature: (signedName && signedName.signature) || undefined,
signing_ts: (signedName && signedName.signing_ts) || undefined,
})
.then((response: SettingsResponse) => {
dispatch({ dispatch({
type: ACTIONS.COMMENT_FETCH_SETTINGS_COMPLETED, type: ACTIONS.COMMENT_FETCH_SETTINGS_COMPLETED,
data: settingsByChannelId, data: {
channelId: channelId,
settings: response,
partialUpdate: !signedName,
},
}); });
}) })
.catch((err) => { .catch((err) => {
// TODO: Use error codes when available.
// TODO: The "validation is disallowed" thing ideally should just be a
// success case that returns a null setting, instead of an error.
// As we are using 'Promise.all', if one channel fails, everyone
// fails. This forces us to remove the batch functionality of this
// function. However, since this "validation is disallowed" thing
// is potentially a temporary one to handle spammers, I retained
// the batch functionality for now.
if (err.message === 'validation is disallowed for non controlling channels') { if (err.message === 'validation is disallowed for non controlling channels') {
const settingsByChannelId = {};
for (let i = 0; i < channelSignatures.length; ++i) {
const channelId = channelSignatures[i].claim_id;
// 'undefined' means "fetching or have not fetched";
// 'null' means "feature not available for this channel";
settingsByChannelId[channelId] = null;
}
dispatch({ dispatch({
type: ACTIONS.COMMENT_FETCH_SETTINGS_COMPLETED, type: ACTIONS.COMMENT_FETCH_SETTINGS_COMPLETED,
data: settingsByChannelId, data: {
channelId: channelId,
settings: null,
partialUpdate: !signedName,
},
});
} else {
devToast(dispatch, `Creator: ${err}`);
dispatch({
type: ACTIONS.COMMENT_FETCH_SETTINGS_FAILED,
}); });
return;
} }
dispatch({
type: ACTIONS.COMMENT_FETCH_SETTINGS_FAILED,
});
}); });
}; };
}; };
@ -1454,22 +1419,13 @@ export const doFetchCreatorSettings = (channelClaimIds: Array<string> = []) => {
* *
* @param channelClaim * @param channelClaim
* @param settings * @param settings
* @returns {function(Dispatch, GetState): Promise<R>|Promise<unknown>|*} * @returns {function(Dispatch, GetState): any}
*/ */
export const doUpdateCreatorSettings = (channelClaim: ChannelClaim, settings: PerChannelSettings) => { export const doUpdateCreatorSettings = (channelClaim: ChannelClaim, settings: PerChannelSettings) => {
return async (dispatch: Dispatch, getState: GetState) => { return async (dispatch: Dispatch, getState: GetState) => {
let channelSignature: ?{ const channelSignature = await channelSignName(channelClaim.claim_id, channelClaim.name);
signature: string,
signing_ts: string,
};
try {
channelSignature = await Lbry.channel_sign({
channel_id: channelClaim.claim_id,
hexdata: toHex(channelClaim.name),
});
} catch (e) {}
if (!channelSignature) { if (!channelSignature) {
devToast(dispatch, 'doUpdateCreatorSettings: failed to sign channel name');
return; return;
} }
@ -1480,12 +1436,7 @@ export const doUpdateCreatorSettings = (channelClaim: ChannelClaim, settings: Pe
signing_ts: channelSignature.signing_ts, signing_ts: channelSignature.signing_ts,
...settings, ...settings,
}).catch((err) => { }).catch((err) => {
dispatch( dispatch(doToast({ message: err.message, isError: true }));
doToast({
message: err.message,
isError: true,
})
);
}); });
}; };
}; };

View file

@ -41,7 +41,6 @@ const defaultState: CommentsState = {
blockingByUri: {}, blockingByUri: {},
unBlockingByUri: {}, unBlockingByUri: {},
togglingForDelegatorMap: {}, togglingForDelegatorMap: {},
commentsDisabledChannelIds: [],
settingsByChannelId: {}, // ChannelId -> PerChannelSettings settingsByChannelId: {}, // ChannelId -> PerChannelSettings
fetchingSettings: false, fetchingSettings: false,
fetchingBlockedWords: false, fetchingBlockedWords: false,
@ -251,32 +250,8 @@ export default handleActions(
claimId, claimId,
uri, uri,
disabled, disabled,
authorClaimId, commenterClaimId,
} = action.data; } = action.data;
const commentsDisabledChannelIds = [...state.commentsDisabledChannelIds];
if (disabled) {
if (!commentsDisabledChannelIds.includes(authorClaimId)) {
commentsDisabledChannelIds.push(authorClaimId);
}
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
if (parentId) {
isLoadingByParentId[parentId] = false;
}
return {
...state,
commentsDisabledChannelIds,
isLoading: false,
isLoadingByParentId,
};
} else {
const index = commentsDisabledChannelIds.indexOf(authorClaimId);
if (index > -1) {
commentsDisabledChannelIds.splice(index, 1);
}
}
const commentById = Object.assign({}, state.commentById); const commentById = Object.assign({}, state.commentById);
const byId = Object.assign({}, state.byId); const byId = Object.assign({}, state.byId);
@ -289,50 +264,61 @@ export default handleActions(
const pinnedCommentsById = Object.assign({}, state.pinnedCommentsById); const pinnedCommentsById = Object.assign({}, state.pinnedCommentsById);
const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId); const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId);
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId); const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
const settingsByChannelId = Object.assign({}, state.settingsByChannelId);
if (!parentId) { settingsByChannelId[commenterClaimId] = {
totalCommentsById[claimId] = totalItems; ...(settingsByChannelId[commenterClaimId] || {}),
topLevelTotalCommentsById[claimId] = totalFilteredItems; comments_enabled: !disabled,
topLevelTotalPagesById[claimId] = totalPages; };
} else {
totalRepliesByParentId[parentId] = totalFilteredItems; if (parentId) {
isLoadingByParentId[parentId] = false; isLoadingByParentId[parentId] = false;
} }
const commonUpdateAction = (comment, commentById, commentIds, index) => { if (!disabled) {
// map the comment_ids to the new comments if (parentId) {
commentById[comment.comment_id] = comment; totalRepliesByParentId[parentId] = totalFilteredItems;
commentIds[index] = comment.comment_id; } else {
}; totalCommentsById[claimId] = totalItems;
topLevelTotalCommentsById[claimId] = totalFilteredItems;
topLevelTotalPagesById[claimId] = totalPages;
}
if (comments) { const commonUpdateAction = (comment, commentById, commentIds, index) => {
// we use an Array to preserve order of listing // map the comment_ids to the new comments
// in reality this doesn't matter and we can just commentById[comment.comment_id] = comment;
// sort comments by their timestamp commentIds[index] = comment.comment_id;
const commentIds = Array(comments.length); };
// --- Top-level comments --- if (comments) {
if (!parentId) { // we use an Array to preserve order of listing
for (let i = 0; i < comments.length; ++i) { // in reality this doesn't matter and we can just
const comment = comments[i]; // sort comments by their timestamp
commonUpdateAction(comment, commentById, commentIds, i); const commentIds = Array(comments.length);
pushToArrayInObject(topLevelCommentsById, claimId, comment.comment_id);
if (comment.is_pinned) { // --- Top-level comments ---
pushToArrayInObject(pinnedCommentsById, claimId, comment.comment_id); if (!parentId) {
for (let i = 0; i < comments.length; ++i) {
const comment = comments[i];
commonUpdateAction(comment, commentById, commentIds, i);
pushToArrayInObject(topLevelCommentsById, claimId, comment.comment_id);
if (comment.is_pinned) {
pushToArrayInObject(pinnedCommentsById, claimId, comment.comment_id);
}
} }
} }
} // --- Replies ---
// --- Replies --- else {
else { for (let i = 0; i < comments.length; ++i) {
for (let i = 0; i < comments.length; ++i) { const comment = comments[i];
const comment = comments[i]; commonUpdateAction(comment, commentById, commentIds, i);
commonUpdateAction(comment, commentById, commentIds, i); pushToArrayInObject(repliesByParentId, parentId, comment.comment_id);
pushToArrayInObject(repliesByParentId, parentId, comment.comment_id); }
} }
}
byId[claimId] ? byId[claimId].push(...commentIds) : (byId[claimId] = commentIds); byId[claimId] ? byId[claimId].push(...commentIds) : (byId[claimId] = commentIds);
commentsByUri[uri] = claimId; commentsByUri[uri] = claimId;
}
} }
return { return {
@ -347,9 +333,9 @@ export default handleActions(
byId, byId,
commentById, commentById,
commentsByUri, commentsByUri,
commentsDisabledChannelIds,
isLoading: false, isLoading: false,
isLoadingByParentId, isLoadingByParentId,
settingsByChannelId,
}; };
}, },
@ -1018,14 +1004,26 @@ export default handleActions(
fetchingSettings: false, fetchingSettings: false,
}), }),
[ACTIONS.COMMENT_FETCH_SETTINGS_COMPLETED]: (state: CommentsState, action: any) => { [ACTIONS.COMMENT_FETCH_SETTINGS_COMPLETED]: (state: CommentsState, action: any) => {
// TODO: This is incorrect, as it could make 'settingsByChannelId' store const { channelId, settings, partialUpdate } = action.data;
// only 1 channel with other channel's data purged. It works for now const settingsByChannelId = Object.assign({}, state.settingsByChannelId);
// because the GUI only shows 1 channel's setting at a time, and *always*
// re-fetches to get latest data before displaying. Either rename this to if (partialUpdate) {
// 'activeChannelCreatorSettings', or append the new data properly. settingsByChannelId[channelId] = {
// The existing may contain additional Creator Settings (e.g. 'words')
...(settingsByChannelId[channelId] || {}),
// Spread new settings.
...settings,
};
} else {
settingsByChannelId[channelId] = settings;
if (settings.words) {
settingsByChannelId[channelId].words = settings.words.split(',');
}
}
return { return {
...state, ...state,
settingsByChannelId: action.data, settingsByChannelId,
fetchingSettings: false, fetchingSettings: 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, makeSelectClaimForUri } from 'lbry-redux'; import { selectClaimsById, isClaimNsfw, selectMyActiveClaims } from 'lbry-redux';
const selectState = (state) => state.comments || {}; const selectState = (state) => state.comments || {};
@ -12,10 +12,6 @@ export const selectIsFetchingComments = createSelector(selectState, (state) => s
export const selectIsFetchingCommentsByParentId = createSelector(selectState, (state) => state.isLoadingByParentId); export const selectIsFetchingCommentsByParentId = createSelector(selectState, (state) => state.isLoadingByParentId);
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 selectPinnedCommentsById = createSelector(selectState, (state) => state.pinnedCommentsById); export const selectPinnedCommentsById = createSelector(selectState, (state) => state.pinnedCommentsById);
@ -426,15 +422,3 @@ 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);
});

View file

@ -438,3 +438,9 @@ $thumbnailWidthSmall: 1rem;
.comment--blocked { .comment--blocked {
opacity: 0.5; opacity: 0.5;
} }
.comment--min-amount-notice {
.icon {
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
}
}

11
ui/util/claim.js Normal file
View file

@ -0,0 +1,11 @@
// @flow
export function getChannelIdFromClaim(claim: ?Claim) {
if (claim) {
if (claim.value_type === 'channel') {
return claim.claim_id;
} else if (claim.signing_channel) {
return claim.signing_channel.claim_id;
}
}
}