2021-04-21 11:25:06 +02:00
// @flow
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' ;
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-06-19 10:28:00 +02:00
import { isNameValid , parseURI } from 'lbry-redux' ;
import ClaimPreview from 'component/claimPreview' ;
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-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-04-21 11:25:06 +02:00
const [ minTipAmountComment , setMinTipAmountComment ] = React . useState ( 0 ) ;
const [ minTipAmountSuperChat , setMinTipAmountSuperChat ] = React . useState ( 0 ) ;
const [ slowModeMinGap , setSlowModeMinGap ] = React . useState ( 0 ) ;
const [ lastUpdated , setLastUpdated ] = React . useState ( 1 ) ;
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 ) ;
setMinTipAmountComment ( settings . min _tip _amount _comment || 0 ) ;
setMinTipAmountSuperChat ( settings . min _tip _amount _super _chat || 0 ) ;
setSlowModeMinGap ( 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 ) {
setMinTipAmountComment ( settings . min _tip _amount _comment ) ;
}
if ( settings . min _tip _amount _super _chat !== undefined ) {
setMinTipAmountSuperChat ( settings . min _tip _amount _super _chat ) ;
}
if ( settings . slow _mode _min _gap !== undefined ) {
setSlowModeMinGap ( settings . slow _mode _min _gap ) ;
}
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 ( ) ) ;
}
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-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-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
backout = { {
title : _ _ ( 'Creator settings' ) ,
backLabel : _ _ ( 'Done' ) ,
} }
className = "card-stack"
>
< ChannelSelector hideAnon / >
{ isBusy && (
< div className = "main--empty" >
< Spinner / >
< / div >
) }
2021-05-25 04:58:42 +02:00
{ isDisabled && (
< Card
title = { _ _ ( 'Settings unavailable for this channel' ) }
subtitle = { _ _ ( "This channel isn't staking enough LBRY Credits to enable Creator Settings." ) }
/ >
) }
{ ! isBusy && ! isDisabled && (
2021-04-21 11:25:06 +02:00
< >
2021-06-03 07:57:50 +02:00
< Card
title = { _ _ ( 'General' ) }
actions = {
< >
< FormField
type = "checkbox"
name = "comments_enabled"
label = { _ _ ( 'Enable comments for channel.' ) }
checked = { commentsEnabled }
onChange = { ( ) => setSettings ( { comments _enabled : ! commentsEnabled } ) }
/ >
2021-07-07 15:33:22 +02:00
< FormField
name = "slow_mode_min_gap"
label = { _ _ ( 'Minimum time gap in seconds between comments (affects livestream chat as well).' ) }
min = { 0 }
step = { 1 }
type = "number"
placeholder = "1"
value = { slowModeMinGap }
onChange = { ( e ) => setSettings ( { slow _mode _min _gap : parseInt ( e . target . value ) } ) }
/ >
2021-06-03 07:57:50 +02:00
< / >
}
/ >
2021-04-21 11:25:06 +02:00
< Card
title = { _ _ ( 'Filter' ) }
actions = {
< 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
2021-05-10 08:31:16 +02:00
disableControlTags
2021-04-21 11:25:06 +02:00
/ >
< / div >
}
/ >
2021-07-16 10:11:02 +02:00
< Card
title = { _ _ ( 'Tip' ) }
actions = {
< >
< FormField
name = "min_tip_amount_comment"
label = {
< 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 = "3.14"
value = { minTipAmountComment }
onChange = { ( e ) => setSettings ( { min _tip _amount _comment : parseFloat ( e . target . value ) } ) }
/ >
< FormField
name = "min_tip_amount_super_chat"
label = {
< I18nMessage tokens = { { lbc : < LbcSymbol / > } } > Minimum % lbc % tip amount for hyperchats < / I18nMessage >
}
helper = { _ _ (
'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 }
step = "any"
type = "number"
placeholder = "1"
value = { minTipAmountSuperChat }
onChange = { ( e ) => setSettings ( { min _tip _amount _super _chat : parseFloat ( e . target . value ) } ) }
/ >
< / >
}
/ >
2021-06-16 04:27:58 +02:00
< Card
title = { _ _ ( 'Delegation' ) }
className = "card--enable-overflow"
actions = {
< div className = "tag--blocked-words" >
2021-06-19 10:28:00 +02:00
< FormField
type = "text"
name = "moderator_search"
className = "form-field--address"
label = { _ _ ( 'Add moderator' ) }
placeholder = { _ _ ( 'Enter a @username or URL' ) }
helper = { _ _ ( 'examples: @channel, @channel#3, https://odysee.com/@Odysee:8, lbry://@Odysee#8' ) }
value = { moderatorSearchTerm }
onChange = { ( e ) => setModeratorSearchTerm ( e . target . value ) }
error = { moderatorSearchError }
2021-06-16 04:27:58 +02:00
/ >
2021-06-19 10:28:00 +02:00
{ 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 >
) }
2021-06-16 04:27:58 +02:00
< TagsSearch
label = { _ _ ( 'Moderators' ) }
2021-07-26 16:27:39 +02:00
labelAddNew = { _ _ ( 'Add moderator' ) }
2021-06-16 04:27:58 +02:00
onRemove = { removeModerator }
onSelect = { addModerator }
tagsPassedIn = { moderatorTags }
disableAutoFocus
hideInputField
hideSuggestions
disableControlTags
/ >
< / div >
}
/ >
2021-04-21 11:25:06 +02:00
< / >
) }
< / Page >
) ;
}