Creator: Enable "min tips" and "min hyperchat" #6824
13 changed files with 426 additions and 272 deletions
17
flow-typed/Comment.js
vendored
17
flow-typed/Comment.js
vendored
|
@ -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 = {
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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?')} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
|
|
|
@ -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
11
ui/util/claim.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue