2021-04-21 11:25:06 +02:00
// @flow
2021-08-19 07:58:40 +02:00
import * as ICONS from 'constants/icons' ;
2021-04-21 11:25:06 +02:00
import * as React from 'react' ;
import Card from 'component/common/card' ;
import TagsSearch from 'component/tagsSearch' ;
import Page from 'component/page' ;
2021-06-19 10:28:00 +02:00
import Button from 'component/button' ;
2021-04-21 11:25:06 +02:00
import ChannelSelector from 'component/channelSelector' ;
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' ;
2021-08-19 07:58:40 +02:00
import Icon from 'component/common/icon' ;
2021-04-21 11:25:06 +02:00
import LbcSymbol from 'component/common/lbc-symbol' ;
import I18nMessage from 'component/i18nMessage' ;
2021-06-19 10:28:00 +02:00
import { isNameValid , parseURI } from 'lbry-redux' ;
import ClaimPreview from 'component/claimPreview' ;
2021-08-11 17:22:27 +02:00
import debounce from 'util/debounce' ;
2021-06-19 10:28:00 +02:00
import { getUriForSearchTerm } from 'util/search' ;
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 ,
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 ,
2021-06-16 04:27:58 +02:00
doToast ,
2021-04-21 11:25:06 +02:00
} = props ;
const [ commentsEnabled , setCommentsEnabled ] = React . useState ( true ) ;
const [ mutedWordTags , setMutedWordTags ] = React . useState ( [ ] ) ;
2021-06-16 04:27:58 +02:00
const [ moderatorTags , setModeratorTags ] = React . useState ( [ ] ) ;
2021-06-19 10:28:00 +02:00
const [ moderatorSearchTerm , setModeratorSearchTerm ] = React . useState ( '' ) ;
const [ moderatorSearchError , setModeratorSearchError ] = React . useState ( '' ) ;
const [ moderatorSearchClaimUri , setModeratorSearchClaimUri ] = 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 ) ;
2021-04-21 11:25:06 +02:00
const [ lastUpdated , setLastUpdated ] = React . useState ( 1 ) ;
2021-08-11 17:22:27 +02:00
const pushSlowModeMinDebounced = React . useMemo ( ( ) => debounce ( pushSlowModeMin , 1000 ) , [ ] ) ;
const pushMinTipDebounced = React . useMemo ( ( ) => debounce ( pushMinTip , 1000 ) , [ ] ) ;
const pushMinSuperDebounced = React . useMemo ( ( ) => debounce ( pushMinSuper , 1000 ) , [ ] ) ;
// **************************************************************************
// **************************************************************************
2021-07-29 16:53:36 +02:00
/ * *
* Updates corresponding GUI states with the given PerChannelSettings values .
*
* @ param settings
* @ param fullSync If true , update all states and consider 'undefined' settings as "cleared/false" ;
* if false , only update defined settings .
* /
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 ) ;
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
}
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-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-06-16 04:27:58 +02:00
function addModerator ( newTags : Array < Tag > ) {
// Ignoring multiple entries for now, although <TagsSearch> supports it.
let modUri ;
try {
modUri = parseURI ( newTags [ 0 ] . name ) ;
} catch ( e ) { }
if ( modUri && modUri . isChannel && modUri . claimName && modUri . claimId ) {
if ( ! moderatorTags . some ( ( modTag ) => modTag . name === newTags [ 0 ] . name ) ) {
setModeratorTags ( [ ... moderatorTags , newTags [ 0 ] ] ) ;
commentModAddDelegate ( modUri . claimId , modUri . claimName , activeChannelClaim ) ;
setLastUpdated ( Date . now ( ) ) ;
}
} else {
doToast ( { message : _ _ ( 'Invalid channel URL "%url%"' , { url : newTags [ 0 ] . name } ) , isError : true } ) ;
}
}
function removeModerator ( tagToRemove : Tag ) {
let modUri ;
try {
modUri = parseURI ( tagToRemove . name ) ;
} catch ( e ) { }
if ( modUri && modUri . isChannel && modUri . claimName && modUri . claimId ) {
const newModeratorTags = moderatorTags . slice ( ) . filter ( ( t ) => t . name !== tagToRemove . name ) ;
setModeratorTags ( newModeratorTags ) ;
commentModRemoveDelegate ( modUri . claimId , modUri . claimName , activeChannelClaim ) ;
setLastUpdated ( Date . now ( ) ) ;
}
}
2021-06-19 10:28:00 +02:00
function handleChannelSearchSelect ( claim ) {
if ( claim && claim . name && claim . claim _id ) {
addModerator ( [ { name : claim . name + '#' + claim . claim _id } ] ) ;
2021-06-16 04:27:58 +02:00
}
}
2021-08-11 17:22:27 +02:00
// **************************************************************************
// **************************************************************************
2021-06-19 10:28:00 +02:00
// 'moderatorSearchTerm' to 'moderatorSearchClaimUri'
React . useEffect ( ( ) => {
if ( ! moderatorSearchTerm ) {
setModeratorSearchError ( '' ) ;
setModeratorSearchClaimUri ( '' ) ;
} else {
const [ searchUri , error ] = getUriForSearchTerm ( moderatorSearchTerm ) ;
setModeratorSearchError ( error ? _ _ ( 'Something not quite right..' ) : '' ) ;
try {
const { streamName , channelName , isChannel } = parseURI ( searchUri ) ;
if ( ! isChannel && streamName && isNameValid ( streamName ) ) {
setModeratorSearchError ( _ _ ( 'Not a channel (prefix with "@", or enter the channel URL)' ) ) ;
setModeratorSearchClaimUri ( '' ) ;
} else if ( isChannel && channelName && isNameValid ( channelName ) ) {
setModeratorSearchClaimUri ( searchUri ) ;
}
} catch ( e ) {
if ( moderatorSearchTerm !== '@' ) {
setModeratorSearchError ( '' ) ;
}
setModeratorSearchClaimUri ( '' ) ;
}
}
} , [ moderatorSearchTerm , setModeratorSearchError ] ) ;
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 ) {
setModeratorTags (
delegates . map ( ( d ) => {
return {
name : d . channelName + '#' + d . channelId ,
} ;
} )
) ;
} else {
setModeratorTags ( [ ] ) ;
}
}
} , [ 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 >
< 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 = { _ _ ( '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 >
< SettingsRow title = { _ _ ( 'Moderators' ) } subtitle = { _ _ ( HELP . MODERATORS ) } multirow >
< div className = "tag--blocked-words" >
< TagsSearch
label = { _ _ ( 'Moderators' ) }
labelAddNew = { _ _ ( 'Add moderator' ) }
onRemove = { removeModerator }
onSelect = { addModerator }
tagsPassedIn = { moderatorTags }
disableAutoFocus
hideInputField
hideSuggestions
disableControlTags
/ >
< FormField
type = "text"
name = "moderator_search"
className = "form-field--address"
label = {
< >
{ _ _ ( 'Search channel' ) }
< Icon
customTooltipText = { _ _ ( HELP . MODERATOR _SEARCH ) }
className = "icon--help"
icon = { ICONS . HELP }
tooltip
size = { 16 }
/ >
< / >
}
placeholder = { _ _ ( 'Enter a @username or URL' ) }
value = { moderatorSearchTerm }
onChange = { ( e ) => setModeratorSearchTerm ( e . target . value ) }
error = { moderatorSearchError }
/ >
{ moderatorSearchClaimUri && (
< div className = "section" >
< ClaimPreview
key = { moderatorSearchClaimUri }
uri = { moderatorSearchClaimUri }
// type={'small'}
// showNullPlaceholder
hideMenu
hideRepostLabel
disableNavigation
properties = { '' }
renderActions = { ( claim ) => {
return (
< Button
requiresAuth
button = "primary"
label = { _ _ ( 'Add as moderator' ) }
onClick = { ( ) => handleChannelSearchSelect ( claim ) }
/ >
) ;
} }
/ >
< / div >
) }
< / div >
< / SettingsRow >
< / >
}
/ >
< / >
) }
< / 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).' ,
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' ,
} ;