2021-04-21 11:25:06 +02:00
// @flow
import * as React from 'react' ;
2022-02-23 07:29:14 +01:00
import humanizeDuration from 'humanize-duration' ;
import * as ICONS from 'constants/icons' ;
import * as MODALS from 'constants/modal_types' ;
import Button from 'component/button' ;
2021-04-21 11:25:06 +02:00
import Card from 'component/common/card' ;
import TagsSearch from 'component/tagsSearch' ;
import Page from 'component/page' ;
import ChannelSelector from 'component/channelSelector' ;
2021-09-08 14:32:13 +02:00
import SearchChannelField from 'component/searchChannelField' ;
2021-08-19 07:58:40 +02:00
import SettingsRow from 'component/settingsRow' ;
2021-04-21 11:25:06 +02:00
import Spinner from 'component/spinner' ;
import { FormField } from 'component/common/form-components/form-field' ;
import LbcSymbol from 'component/common/lbc-symbol' ;
import I18nMessage from 'component/i18nMessage' ;
2021-10-17 10:36:14 +02:00
import { parseURI } from 'util/lbryURI' ;
2021-08-11 17:22:27 +02:00
import debounce from 'util/debounce' ;
2021-04-21 11:25:06 +02:00
const DEBOUNCE _REFRESH _MS = 1000 ;
2021-07-16 10:11:02 +02:00
const LBC _MAX = 21000000 ;
const LBC _MIN = 0 ;
const LBC _STEP = 1.0 ;
2021-05-25 03:31:59 +02:00
2021-08-11 17:22:27 +02:00
// ****************************************************************************
// ****************************************************************************
2021-04-21 11:25:06 +02:00
type Props = {
activeChannelClaim : ChannelClaim ,
settingsByChannelId : { [ string ] : PerChannelSettings } ,
fetchingCreatorSettings : boolean ,
fetchingBlockedWords : boolean ,
2021-06-16 04:27:58 +02:00
moderationDelegatesById : { [ string ] : Array < { channelId : string , channelName : string } > } ,
2021-04-21 11:25:06 +02:00
commentBlockWords : ( ChannelClaim , Array < string > ) => void ,
commentUnblockWords : ( ChannelClaim , Array < string > ) => void ,
2021-06-16 04:27:58 +02:00
commentModAddDelegate : ( string , string , ChannelClaim ) => void ,
commentModRemoveDelegate : ( string , string , ChannelClaim ) => void ,
commentModListDelegates : ( ChannelClaim ) => void ,
2021-07-29 16:53:36 +02:00
fetchCreatorSettings : ( channelId : string ) => void ,
2021-04-21 11:25:06 +02:00
updateCreatorSettings : ( ChannelClaim , PerChannelSettings ) => void ,
2021-06-16 04:27:58 +02:00
doToast : ( { message : string } ) => void ,
2022-02-23 07:29:14 +01:00
doOpenModal : ( id : string , { } ) => void ,
2021-04-21 11:25:06 +02:00
} ;
export default function SettingsCreatorPage ( props : Props ) {
const {
activeChannelClaim ,
settingsByChannelId ,
2021-06-16 04:27:58 +02:00
moderationDelegatesById ,
2021-04-21 11:25:06 +02:00
commentBlockWords ,
commentUnblockWords ,
2021-06-16 04:27:58 +02:00
commentModAddDelegate ,
commentModRemoveDelegate ,
commentModListDelegates ,
2021-04-21 11:25:06 +02:00
fetchCreatorSettings ,
updateCreatorSettings ,
2022-02-23 07:29:14 +01:00
doOpenModal ,
2021-04-21 11:25:06 +02:00
} = props ;
const [ commentsEnabled , setCommentsEnabled ] = React . useState ( true ) ;
const [ mutedWordTags , setMutedWordTags ] = React . useState ( [ ] ) ;
2021-09-08 14:32:13 +02:00
const [ moderatorUris , setModeratorUris ] = React . useState ( [ ] ) ;
2021-08-11 17:22:27 +02:00
const [ minTip , setMinTip ] = React . useState ( 0 ) ;
const [ minSuper , setMinSuper ] = React . useState ( 0 ) ;
const [ slowModeMin , setSlowModeMin ] = React . useState ( 0 ) ;
2022-02-23 07:29:14 +01:00
const [ minChannelAgeMinutes , setMinChannelAgeMinutes ] = React . useState ( 0 ) ;
2021-04-21 11:25:06 +02:00
const [ lastUpdated , setLastUpdated ] = React . useState ( 1 ) ;
2021-09-08 14:32:13 +02:00
const pushSlowModeMinDebounced = React . useMemo ( ( ) => debounce ( pushSlowModeMin , 1000 ) , [ ] ) ; // eslint-disable-line react-hooks/exhaustive-deps
const pushMinTipDebounced = React . useMemo ( ( ) => debounce ( pushMinTip , 1000 ) , [ ] ) ; // eslint-disable-line react-hooks/exhaustive-deps
const pushMinSuperDebounced = React . useMemo ( ( ) => debounce ( pushMinSuper , 1000 ) , [ ] ) ; // eslint-disable-line react-hooks/exhaustive-deps
2021-08-11 17:22:27 +02:00
// **************************************************************************
// **************************************************************************
2021-07-29 16:53:36 +02:00
/ * *
* Updates corresponding GUI states with the given PerChannelSettings values .
*
* @ param settings
2021-09-08 14:32:13 +02:00
* @ param fullSync If true , update all states and consider 'undefined'
* settings as "cleared/false" ; if false , only update defined settings .
2021-07-29 16:53:36 +02:00
* /
function settingsToStates ( settings : PerChannelSettings , fullSync : boolean ) {
const doSetMutedWordTags = ( words : Array < string > ) => {
const tagArray = Array . from ( new Set ( words ) ) ;
2021-04-21 11:25:06 +02:00
setMutedWordTags (
tagArray
. filter ( ( t ) => t !== '' )
. map ( ( x ) => {
return { name : x } ;
} )
) ;
2021-07-29 16:53:36 +02:00
} ;
if ( fullSync ) {
setCommentsEnabled ( settings . comments _enabled || false ) ;
2021-08-11 17:22:27 +02:00
setMinTip ( settings . min _tip _amount _comment || 0 ) ;
setMinSuper ( settings . min _tip _amount _super _chat || 0 ) ;
setSlowModeMin ( settings . slow _mode _min _gap || 0 ) ;
2022-02-23 07:29:14 +01:00
setMinChannelAgeMinutes ( settings . time _since _first _comment || 0 ) ;
2021-07-29 16:53:36 +02:00
doSetMutedWordTags ( settings . words || [ ] ) ;
} else {
if ( settings . comments _enabled !== undefined ) {
setCommentsEnabled ( settings . comments _enabled ) ;
}
if ( settings . min _tip _amount _comment !== undefined ) {
2021-08-11 17:22:27 +02:00
setMinTip ( settings . min _tip _amount _comment ) ;
2021-07-29 16:53:36 +02:00
}
if ( settings . min _tip _amount _super _chat !== undefined ) {
2021-08-11 17:22:27 +02:00
setMinSuper ( settings . min _tip _amount _super _chat ) ;
2021-07-29 16:53:36 +02:00
}
if ( settings . slow _mode _min _gap !== undefined ) {
2021-08-11 17:22:27 +02:00
setSlowModeMin ( settings . slow _mode _min _gap ) ;
2021-07-29 16:53:36 +02:00
}
2022-02-23 07:29:14 +01:00
if ( settings . time _since _first _comment ) {
setMinChannelAgeMinutes ( settings . time _since _first _comment ) ;
}
2021-07-29 16:53:36 +02:00
if ( settings . words ) {
doSetMutedWordTags ( settings . words ) ;
}
2021-04-21 11:25:06 +02:00
}
}
function setSettings ( newSettings : PerChannelSettings ) {
2021-07-29 16:53:36 +02:00
settingsToStates ( newSettings , false ) ;
2021-04-21 11:25:06 +02:00
updateCreatorSettings ( activeChannelClaim , newSettings ) ;
setLastUpdated ( Date . now ( ) ) ;
}
2021-08-11 17:22:27 +02:00
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 } ) ;
}
2021-09-08 14:32:13 +02:00
function parseModUri ( uri ) {
try {
return parseURI ( uri ) ;
} catch ( e ) { }
return undefined ;
}
function handleModeratorAdded ( channelUri : string ) {
setModeratorUris ( [ ... moderatorUris , channelUri ] ) ;
const parsedUri = parseModUri ( channelUri ) ;
if ( parsedUri && parsedUri . claimId && parsedUri . claimName ) {
commentModAddDelegate ( parsedUri . claimId , parsedUri . claimName , activeChannelClaim ) ;
setLastUpdated ( Date . now ( ) ) ;
}
}
function handleModeratorRemoved ( channelUri : string ) {
setModeratorUris ( moderatorUris . filter ( ( x ) => x !== channelUri ) ) ;
const parsedUri = parseModUri ( channelUri ) ;
if ( parsedUri && parsedUri . claimId && parsedUri . claimName ) {
commentModRemoveDelegate ( parsedUri . claimId , parsedUri . claimName , activeChannelClaim ) ;
setLastUpdated ( Date . now ( ) ) ;
}
}
2021-04-21 11:25:06 +02:00
function addMutedWords ( newTags : Array < Tag > ) {
const validatedNewTags = [ ] ;
newTags . forEach ( ( newTag ) => {
if ( ! mutedWordTags . some ( ( tag ) => tag . name === newTag . name ) ) {
validatedNewTags . push ( newTag ) ;
}
} ) ;
if ( validatedNewTags . length !== 0 ) {
setMutedWordTags ( [ ... mutedWordTags , ... validatedNewTags ] ) ;
commentBlockWords (
activeChannelClaim ,
validatedNewTags . map ( ( x ) => x . name )
) ;
setLastUpdated ( Date . now ( ) ) ;
}
}
function removeMutedWord ( tagToRemove : Tag ) {
const newMutedWordTags = mutedWordTags . slice ( ) . filter ( ( t ) => t . name !== tagToRemove . name ) ;
setMutedWordTags ( newMutedWordTags ) ;
commentUnblockWords ( activeChannelClaim , [ '' , tagToRemove . name ] ) ;
setLastUpdated ( Date . now ( ) ) ;
}
2021-08-11 17:22:27 +02:00
// **************************************************************************
// **************************************************************************
2021-06-16 04:27:58 +02:00
// Update local moderator states with data from API.
React . useEffect ( ( ) => {
commentModListDelegates ( activeChannelClaim ) ;
} , [ activeChannelClaim , commentModListDelegates ] ) ;
React . useEffect ( ( ) => {
if ( activeChannelClaim ) {
const delegates = moderationDelegatesById [ activeChannelClaim . claim _id ] ;
if ( delegates ) {
2021-09-08 14:32:13 +02:00
setModeratorUris ( delegates . map ( ( d ) => ` ${ d . channelName } # ${ d . channelId } ` ) ) ;
2021-06-16 04:27:58 +02:00
} else {
2021-09-08 14:32:13 +02:00
setModeratorUris ( [ ] ) ;
2021-06-16 04:27:58 +02:00
}
}
} , [ activeChannelClaim , moderationDelegatesById ] ) ;
2021-04-21 11:25:06 +02:00
// Update local states with data from API.
React . useEffect ( ( ) => {
if ( lastUpdated !== 0 && Date . now ( ) - lastUpdated < DEBOUNCE _REFRESH _MS ) {
// Still debouncing. Skip update.
return ;
}
if ( activeChannelClaim && settingsByChannelId && settingsByChannelId [ activeChannelClaim . claim _id ] ) {
const channelSettings = settingsByChannelId [ activeChannelClaim . claim _id ] ;
2021-07-29 16:53:36 +02:00
settingsToStates ( channelSettings , true ) ;
2021-04-21 11:25:06 +02:00
}
} , [ activeChannelClaim , settingsByChannelId , lastUpdated ] ) ;
2021-07-29 16:53:36 +02:00
// Re-sync list on first idle time; mainly to correct any invalid settings.
2021-04-21 11:25:06 +02:00
React . useEffect ( ( ) => {
if ( lastUpdated && activeChannelClaim ) {
const timer = setTimeout ( ( ) => {
2021-07-29 16:53:36 +02:00
fetchCreatorSettings ( activeChannelClaim . claim _id ) ;
2021-04-21 11:25:06 +02:00
} , DEBOUNCE _REFRESH _MS ) ;
return ( ) => clearTimeout ( timer ) ;
}
} , [ lastUpdated , activeChannelClaim , fetchCreatorSettings ] ) ;
2021-08-11 17:22:27 +02:00
// **************************************************************************
// **************************************************************************
2021-05-25 04:58:42 +02:00
const isBusy =
! activeChannelClaim || ! settingsByChannelId || settingsByChannelId [ activeChannelClaim . claim _id ] === undefined ;
const isDisabled =
activeChannelClaim && settingsByChannelId && settingsByChannelId [ activeChannelClaim . claim _id ] === null ;
2021-04-21 11:25:06 +02:00
return (
< Page
noFooter
noSideNavigation
2021-08-19 07:58:40 +02:00
settingsPage
backout = { { title : _ _ ( 'Creator settings' ) , backLabel : _ _ ( 'Back' ) } }
2021-04-21 11:25:06 +02:00
className = "card-stack"
>
2021-08-19 07:58:40 +02:00
< div className = "card-stack" >
< ChannelSelector hideAnon / >
{ isBusy && (
< div className = "main--empty" >
< Spinner / >
< / div >
) }
{ isDisabled && (
2021-04-21 11:25:06 +02:00
< Card
2021-08-19 07:58:40 +02:00
title = { _ _ ( 'Settings unavailable for this channel' ) }
subtitle = { _ _ ( "This channel isn't staking enough LBRY Credits to enable Creator Settings." ) }
2021-04-21 11:25:06 +02:00
/ >
2021-08-19 07:58:40 +02:00
) }
{ ! isBusy && ! isDisabled && (
< >
< Card
isBodyList
body = {
< >
< SettingsRow title = { _ _ ( 'Enable comments for channel.' ) } >
< FormField
type = "checkbox"
name = "comments_enabled"
checked = { commentsEnabled }
onChange = { ( ) => setSettings ( { comments _enabled : ! commentsEnabled } ) }
/ >
< / SettingsRow >
< SettingsRow title = { _ _ ( 'Slow mode' ) } subtitle = { _ _ ( HELP . SLOW _MODE ) } >
< FormField
name = "slow_mode_min_gap"
min = { 0 }
step = { 1 }
type = "number"
placeholder = "1"
value = { slowModeMin }
onChange = { ( e ) => {
const value = parseInt ( e . target . value ) ;
setSlowModeMin ( value ) ;
pushSlowModeMinDebounced ( value , activeChannelClaim ) ;
} }
onBlur = { ( ) => setLastUpdated ( Date . now ( ) ) }
/ >
< / SettingsRow >
2022-02-23 07:29:14 +01:00
< SettingsRow title = { _ _ ( 'Minimum channel age for comments' ) } subtitle = { _ _ ( HELP . CHANNEL _AGE ) } >
< div className = "section__actions" >
< FormField
name = "time_since_first_comment"
className = "form-field--copyable"
disabled = { minChannelAgeMinutes <= 0 }
type = "text"
readOnly
value = {
minChannelAgeMinutes > 0
? humanizeDuration ( minChannelAgeMinutes * 60 * 1000 , { round : true } )
: _ _ ( 'No limit' )
}
inputButton = {
< Button
button = "secondary"
icon = { ICONS . EDIT }
title = { _ _ ( 'Change' ) }
onClick = { ( ) => {
doOpenModal ( MODALS . MIN _CHANNEL _AGE , {
onConfirm : ( limitInMinutes : number , closeModal : ( ) => void ) => {
setMinChannelAgeMinutes ( limitInMinutes ) ;
updateCreatorSettings ( activeChannelClaim , {
time _since _first _comment : limitInMinutes ,
} ) ;
closeModal ( ) ;
} ,
} ) ;
} }
/ >
}
/ >
< / div >
< / SettingsRow >
2021-08-19 07:58:40 +02:00
< SettingsRow
title = {
< I18nMessage tokens = { { lbc : < LbcSymbol / > } } > Minimum % lbc % tip amount for comments < / I18nMessage >
2021-08-11 17:22:27 +02:00
}
2021-08-19 07:58:40 +02:00
subtitle = { _ _ ( HELP . MIN _TIP ) }
>
< FormField
name = "min_tip_amount_comment"
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 ) ;
}
2021-06-19 10:28:00 +02:00
} }
2021-08-19 07:58:40 +02:00
onBlur = { ( ) => setLastUpdated ( Date . now ( ) ) }
2021-06-19 10:28:00 +02:00
/ >
2021-08-19 07:58:40 +02:00
< / SettingsRow >
< SettingsRow
title = {
< I18nMessage tokens = { { lbc : < LbcSymbol / > } } > Minimum % lbc % tip amount for hyperchats < / I18nMessage >
}
subtitle = {
< >
{ _ _ ( HELP . MIN _SUPER ) }
{ minTip !== 0 && (
< p className = "help--inline" >
< em > { _ _ ( HELP . MIN _SUPER _OFF ) } < / em >
< / p >
) }
< / >
}
>
< FormField
name = "min_tip_amount_super_chat"
className = "form-field--price-amount"
min = { 0 }
step = "any"
type = "number"
placeholder = "1"
value = { minSuper }
disabled = { minTip !== 0 }
onChange = { ( e ) => {
const newMinSuper = parseFloat ( e . target . value ) ;
setMinSuper ( newMinSuper ) ;
pushMinSuperDebounced ( newMinSuper , activeChannelClaim ) ;
} }
onBlur = { ( ) => setLastUpdated ( Date . now ( ) ) }
/ >
< / SettingsRow >
< SettingsRow title = { _ _ ( 'Moderators' ) } subtitle = { _ _ ( HELP . MODERATORS ) } multirow >
2021-09-08 14:32:13 +02:00
< SearchChannelField
label = { _ _ ( 'Moderators' ) }
labelAddNew = { _ _ ( 'Add moderator' ) }
labelFoundAction = { _ _ ( 'Add' ) }
values = { moderatorUris }
onAdd = { handleModeratorAdded }
onRemove = { handleModeratorRemoved }
/ >
2021-08-19 07:58:40 +02:00
< / SettingsRow >
2021-09-08 11:08:20 +02:00
< SettingsRow title = { _ _ ( 'Filter' ) } subtitle = { _ _ ( HELP . BLOCKED _WORDS ) } multirow >
< div className = "tag--blocked-words" >
< TagsSearch
label = { _ _ ( 'Muted words' ) }
labelAddNew = { _ _ ( 'Add words' ) }
labelSuggestions = { _ _ ( 'Suggestions' ) }
onRemove = { removeMutedWord }
onSelect = { addMutedWords }
disableAutoFocus
tagsPassedIn = { mutedWordTags }
placeholder = { _ _ ( 'Add words to block' ) }
hideSuggestions
disableControlTags
/ >
< / div >
< / SettingsRow >
2021-08-19 07:58:40 +02:00
< / >
}
/ >
< / >
) }
< / div >
2021-04-21 11:25:06 +02:00
< / Page >
) ;
}
2021-08-19 07:58:40 +02:00
// prettier-ignore
const HELP = {
SLOW _MODE : 'Minimum time gap in seconds between comments (affects livestream chat as well).' ,
2022-02-23 07:29:14 +01:00
CHANNEL _AGE : 'Channels with a lifespan lower than the specified duration will not be able to comment on your content.' ,
2021-08-19 07:58:40 +02:00
MIN _TIP : 'Enabling a minimum amount to comment will force all comments, including livestreams, to have tips associated with them. This can help prevent spam.' ,
MIN _SUPER : '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.' ,
MIN _SUPER _OFF : '(This settings is not applicable if all comments require a tip.)' ,
BLOCKED _WORDS : 'Comments and livestream chat containing these words will be blocked.' ,
MODERATORS : 'Moderators can block channels on your behalf. Blocked channels will appear in your "Blocked and Muted" list.' ,
MODERATOR _SEARCH : 'Enter a channel name or URL to add as a moderator.\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8' ,
} ;