2018-04-28 17:56:03 -04:00
'use strict' ;
// ============================================================================
// Emoji Handling
// ============================================================================
import Module from 'utilities/module' ;
import { has , maybe _call , deep _copy } from 'utilities/object' ;
2019-04-29 18:14:04 -04:00
import { createElement , ClickOutside } from 'utilities/dom' ;
2018-04-28 17:56:03 -04:00
import Tooltip from 'utilities/tooltip' ;
import * as ACTIONS from './types' ;
import * as RENDERERS from './renderers' ;
2019-04-29 18:14:04 -04:00
import { transformPhrase } from 'src/i18n' ;
2018-04-28 17:56:03 -04:00
2019-04-29 18:14:04 -04:00
const VAR _REPLACE = /\{\{(.*?)(?:\|(.*?))?\}\}/g ;
2018-04-28 17:56:03 -04:00
export default class Actions extends Module {
constructor ( ... args ) {
super ( ... args ) ;
this . inject ( 'settings' ) ;
this . inject ( 'tooltips' ) ;
this . inject ( 'i18n' ) ;
this . actions = { } ;
this . renderers = { } ;
2021-03-02 16:55:25 -05:00
this . settings . add ( 'chat.actions.size' , {
default : 16 ,
ui : {
path : 'Chat > Actions @{"always_list_pages": true} >> Appearance' ,
title : 'Action Size' ,
description : "How tall actions should be, in pixels. This may be affected by your browser's zoom and font size settings." ,
component : 'setting-text-box' ,
2021-04-24 14:37:01 -04:00
process : 'to_int' ,
bounds : [ 1 ]
2021-03-02 16:55:25 -05:00
}
} ) ;
2022-12-18 17:30:34 -05:00
this . settings . add ( 'chat.actions.hover-size' , {
default : 30 ,
ui : {
path : 'Chat > Actions > Message Hover >> Appearance' ,
title : 'Action Size' ,
description : "How tall hover actions should be, in pixels. This may be affected by your browser's zoom and font size settings." ,
component : 'setting-text-box' ,
process : 'to_int' ,
bounds : [ 1 ]
}
} ) ;
2019-04-29 18:14:04 -04:00
this . settings . add ( 'chat.actions.reasons' , {
default : [
{ v : { text : 'One-Man Spam' , i18n : 'chat.reasons.spam' } } ,
{ v : { text : 'Posting Bad Links' , i18n : 'chat.reasons.links' } } ,
{ v : { text : 'Ban Evasion' , i18n : 'chat.reasons.evasion' } } ,
{ v : { text : 'Threats / Personal Info' , i18n : 'chat.reasons.personal' } } ,
{ v : { text : 'Hate / Harassment' , i18n : 'chat.reasons.hate' } } ,
{ v : { text : 'Ignoring Broadcaster / Moderators' , i18n : 'chat.reason.ignore' } }
] ,
type : 'array_merge' ,
always _inherit : true ,
2019-05-03 22:36:26 -04:00
// Clean up after Vue being stupid.
process ( ctx , val ) {
if ( Array . isArray ( val ) )
for ( const entry of val )
if ( entry . i18n && typeof entry . i18n !== 'string' )
delete entry . i18n ;
return val ;
} ,
2019-04-29 18:14:04 -04:00
ui : {
2019-05-03 19:30:46 -04:00
path : 'Chat > Actions > Reasons >> Custom Reasons' ,
2019-04-29 18:14:04 -04:00
component : 'chat-reasons' ,
}
} ) ;
2022-12-07 16:52:07 -05:00
this . settings . add ( 'chat.actions.hover' , {
process : ( ctx , val ) =>
val . filter ( x => x . appearance &&
this . renderers [ x . appearance . type ] &&
( ! this . renderers [ x . appearance . type ] . load || this . renderers [ x . appearance . type ] . load ( x . appearance ) ) &&
( ! x . action || this . actions [ x . action ] )
) ,
default : [
{ v : { action : 'pin' , appearance : { type : 'icon' , icon : 'ffz-i-pin' } , options : { } , display : { mod _icons : true } } } ,
{ v : { action : 'reply' , appearance : { type : 'dynamic' } , options : { } , display : { } } }
] ,
type : 'array_merge' ,
inherit _default : true ,
ui : {
path : 'Chat > Actions > Message Hover @{"description": "Here, you can define custom actions that will appear on top of messages in chat when you hover over them. If you aren\'t seeing an action you\'ve defined here in chat, please make sure that you have enabled Mod Icons in the chat settings menu."}' ,
component : 'chat-actions' ,
context : [ 'user' , 'room' , 'message' ] ,
inline : true ,
modifiers : true ,
hover _modifier : false ,
data : ( ) => {
const chat = this . resolve ( 'site.chat' ) ;
return {
color : val => chat && chat . colors ? chat . colors . process ( val ) : val ,
actions : deep _copy ( this . actions ) ,
renderers : deep _copy ( this . renderers )
}
}
}
} ) ;
2018-04-28 17:56:03 -04:00
this . settings . add ( 'chat.actions.inline' , {
// Filter out actions
process : ( ctx , val ) =>
val . filter ( x => x . appearance &&
this . renderers [ x . appearance . type ] &&
( ! this . renderers [ x . appearance . type ] . load || this . renderers [ x . appearance . type ] . load ( x . appearance ) ) &&
( ! x . action || this . actions [ x . action ] )
) ,
default : [
2018-09-03 14:32:58 -04:00
{ v : { action : 'ban' , appearance : { type : 'icon' , icon : 'ffz-i-block' } , options : { } , display : { mod : true , mod _icons : true , deleted : false } } } ,
{ v : { action : 'unban' , appearance : { type : 'icon' , icon : 'ffz-i-ok' } , options : { } , display : { mod : true , mod _icons : true , deleted : true } } } ,
2018-04-28 17:56:03 -04:00
{ v : { action : 'timeout' , appearance : { type : 'icon' , icon : 'ffz-i-clock' } , display : { mod : true , mod _icons : true } } } ,
2022-12-07 16:52:07 -05:00
{ v : { action : 'msg_delete' , appearance : { type : 'icon' , icon : 'ffz-i-trash' } , options : { } , display : { mod : true , mod _icons : true } } }
2018-04-28 17:56:03 -04:00
] ,
type : 'array_merge' ,
2020-08-17 13:33:30 -04:00
inherit _default : true ,
2018-04-28 17:56:03 -04:00
ui : {
2019-04-29 18:14:04 -04:00
path : 'Chat > Actions > In-Line @{"description": "Here, you can define custom actions that will appear along messages in chat. If you aren\'t seeing an action you\'ve defined here in chat, please make sure that you have enabled Mod Icons in the chat settings menu."}' ,
2018-04-28 17:56:03 -04:00
component : 'chat-actions' ,
2018-10-16 02:13:14 -04:00
context : [ 'user' , 'room' , 'message' ] ,
2018-04-28 17:56:03 -04:00
inline : true ,
2019-05-16 14:46:26 -04:00
modifiers : true ,
2018-04-28 17:56:03 -04:00
data : ( ) => {
const chat = this . resolve ( 'site.chat' ) ;
return {
color : val => chat && chat . colors ? chat . colors . process ( val ) : val ,
actions : deep _copy ( this . actions ) ,
renderers : deep _copy ( this . renderers )
}
}
}
} ) ;
2019-08-06 15:39:45 -04:00
this . settings . add ( 'chat.actions.user-context' , {
// Filter out actions
process : ( ctx , val ) =>
val . filter ( x => x . type || ( x . appearance &&
this . renderers [ x . appearance . type ] &&
( ! this . renderers [ x . appearance . type ] . load || this . renderers [ x . appearance . type ] . load ( x . appearance ) ) &&
( ! x . action || this . actions [ x . action ] )
) ) ,
default : [ ] ,
type : 'array_merge' ,
2020-08-17 13:33:30 -04:00
inherit _default : true ,
2019-08-06 15:39:45 -04:00
ui : {
path : 'Chat > Actions > User Context @{"description": "Here, you can define custom actions that will appear in a context menu when you right-click a username in chat."}' ,
component : 'chat-actions' ,
context : [ 'user' , 'room' , 'message' ] ,
mod _icons : true ,
data : ( ) => {
const chat = this . resolve ( 'site.chat' ) ;
return {
color : val => chat && chat . colors ? chat . colors . process ( val ) : val ,
actions : deep _copy ( this . actions ) ,
renderers : deep _copy ( this . renderers )
}
}
}
} )
2019-05-07 15:04:12 -04:00
this . settings . add ( 'chat.actions.room' , {
// Filter out actions
process : ( ctx , val ) =>
val . filter ( x => x . type || ( x . appearance &&
this . renderers [ x . appearance . type ] &&
( ! this . renderers [ x . appearance . type ] . load || this . renderers [ x . appearance . type ] . load ( x . appearance ) ) &&
( ! x . action || this . actions [ x . action ] )
) ) ,
default : [ ] ,
type : 'array_merge' ,
2020-08-17 13:33:30 -04:00
inherit _default : true ,
2019-05-07 15:04:12 -04:00
ui : {
path : 'Chat > Actions > Room @{"description": "Here, you can define custom actions that will appear above the chat input box."}' ,
component : 'chat-actions' ,
2019-08-12 22:52:57 -04:00
context : [ 'room' , 'room-mode' ] ,
2021-03-02 19:50:25 -05:00
inline : false ,
2019-05-07 15:04:12 -04:00
data : ( ) => {
const chat = this . resolve ( 'site.chat' ) ;
return {
color : val => chat && chat . colors ? chat . colors . process ( val ) : val ,
actions : deep _copy ( this . actions ) ,
renderers : deep _copy ( this . renderers )
}
}
}
} ) ;
2019-08-13 16:22:04 -04:00
this . settings . add ( 'chat.actions.room-above' , {
default : false ,
ui : {
path : 'Chat > Actions > Room >> General' ,
component : 'setting-check-box' ,
title : 'Display Room Actions above the chat input box.'
}
} ) ;
2019-05-03 19:30:46 -04:00
this . settings . add ( 'chat.actions.rules-as-reasons' , {
default : true ,
ui : {
path : 'Chat > Actions > Reasons >> Rules' ,
component : 'setting-check-box' ,
title : "Include the current room's rules in the list of reasons."
}
} ) ;
2018-04-28 17:56:03 -04:00
this . handleClick = this . handleClick . bind ( this ) ;
this . handleContext = this . handleContext . bind ( this ) ;
2019-08-06 15:39:45 -04:00
this . handleUserContext = this . handleUserContext . bind ( this ) ;
2018-04-28 17:56:03 -04:00
}
onEnable ( ) {
this . tooltips . types . action = ( target , tip ) => {
const data = this . getData ( target ) ;
if ( ! data )
return this . i18n . t ( 'chat.actions.unknown' , 'Unknown Action Type' ) ;
if ( ! data . definition . tooltip )
return ` Error: The " ${ data . action } " action provider does not have tooltip support. ` ;
if ( data . tip && data . tip . length )
2019-08-27 16:18:12 -04:00
return this . replaceVariables ( data . tip , data ) ;
2018-04-28 17:56:03 -04:00
return maybe _call ( data . definition . tooltip , this , data , target , tip ) ;
}
for ( const key in ACTIONS )
if ( has ( ACTIONS , key ) )
this . addAction ( key , ACTIONS [ key ] ) ;
for ( const key in RENDERERS )
if ( has ( RENDERERS , key ) )
this . addRenderer ( key , RENDERERS [ key ] ) ;
}
addAction ( key , data ) {
if ( has ( this . actions , key ) )
return this . log . warn ( ` Attempted to add action " ${ key } " which is already defined. ` ) ;
this . actions [ key ] = data ;
2022-12-11 15:48:32 -05:00
for ( const ctx of this . settings . _ _contexts ) {
2018-04-28 17:56:03 -04:00
ctx . update ( 'chat.actions.inline' ) ;
2022-12-11 15:48:32 -05:00
ctx . update ( 'chat.actions.hover' ) ;
ctx . update ( 'chat.actions.user-context' ) ;
ctx . update ( 'chat.actions.room' ) ;
}
2018-04-28 17:56:03 -04:00
}
addRenderer ( key , data ) {
if ( has ( this . renderers , key ) )
return this . log . warn ( ` Attempted to add renderer " ${ key } " which is already defined. ` ) ;
this . renderers [ key ] = data ;
2022-12-11 15:48:32 -05:00
for ( const ctx of this . settings . _ _contexts ) {
ctx . update ( 'chat.actions.inline' ) ;
2018-04-28 17:56:03 -04:00
ctx . update ( 'chat.actions.inline' ) ;
2022-12-11 15:48:32 -05:00
ctx . update ( 'chat.actions.hover' ) ;
ctx . update ( 'chat.actions.user-context' ) ;
ctx . update ( 'chat.actions.room' ) ;
}
2018-04-28 17:56:03 -04:00
}
2019-04-29 18:14:04 -04:00
replaceVariables ( text , data ) {
return transformPhrase (
text ,
data ,
this . i18n . locale ,
VAR _REPLACE ,
2022-04-19 15:34:20 -04:00
{
upper ( val ) {
return val . toString ( ) . toUpperCase ( ) ;
} ,
uppercase ( val ) {
return val . toString ( ) . toUpperCase ( ) ;
} ,
lower ( val ) {
return val . toString ( ) . toLowerCase ( ) ;
} ,
lowercase ( val ) {
return val . toString ( ) . toLowerCase ( ) ;
} ,
snakecase ( val ) {
return val . toString ( ) . toSnakeCase ( ) ;
} ,
slugify ( val , locale , options , extra ) {
return val . toString ( ) . toSlug ( extra && extra . length ? extra : '-' ) ;
} ,
word ( val , locale , options , extra ) {
if ( ! extra || ! extra . length )
return val ;
let start , end ;
const bits = extra . split ( ',' ) ;
try {
start = parseInt ( bits [ 0 ] , 10 ) ;
if ( isNaN ( start ) || ! isFinite ( start ) )
return val ;
} catch ( err ) {
this . log . warn ( 'Invalid value for word(start)' , bits [ 0 ] ) ;
return val ;
}
if ( bits . length > 1 ) {
const bit = bits [ 1 ] . trim ( ) ;
if ( ! bit . length )
end = - 1 ;
else
try {
end = parseInt ( bits [ 1 ] , 10 ) ;
if ( isNaN ( end ) || ! isFinite ( end ) )
return val ;
} catch ( err ) {
this . log . warn ( 'Invalid value for word(end)' , bits [ 1 ] ) ;
return val ;
}
}
const words = val . split ( /\s+/ ) ;
if ( start < 0 )
start = words . length + start ;
if ( start < 0 )
start = 0 ;
if ( start >= words . length )
start = words . length - 1 ;
if ( end != null ) {
if ( end < 0 )
end = words . length + end ;
if ( end < start )
end = start ;
if ( end > words . length )
end = words . length ;
return words . slice ( start , end + 1 ) . join ( ' ' ) ;
}
return words [ start ] ;
}
}
2019-04-29 18:14:04 -04:00
) ;
}
renderInlineReasons ( data , t , tip ) {
const reasons = this . parent . context . get ( 'chat.actions.reasons' ) ,
reason _elements = [ ] ,
2019-05-03 19:30:46 -04:00
room = this . parent . context . get ( 'chat.actions.rules-as-reasons' ) && this . parent . getRoom ( data . room . id , data . room . login , true ) ,
2019-04-29 18:14:04 -04:00
rules = room && room . rules ;
if ( ! reasons && ! rules ) {
tip . hide ( ) ;
return null ;
}
const click _fn = reason => e => {
tip . hide ( ) ;
data . definition . click . call ( this , e , Object . assign ( { reason } , data ) ) ;
e . preventDefault ( ) ;
return false ;
} ;
2019-04-29 21:14:23 -04:00
if ( reasons && reasons . length ) {
for ( const reason of reasons ) {
2019-05-03 19:30:46 -04:00
const text = this . replaceVariables ( ( typeof reason . i18n === 'string' ) ? this . i18n . t ( reason . i18n , reason . text ) : reason . text , data ) ;
2019-04-29 21:14:23 -04:00
reason _elements . push ( < li class = "tw-full-width tw-relative" >
< a
href = "#"
2021-02-11 19:40:12 -05:00
class = "tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive tw-pd-05"
2019-06-20 15:15:54 -04:00
onClick = { click _fn ( text ) }
2019-04-29 21:14:23 -04:00
>
{ text }
< / a >
< / li > )
}
2019-04-29 18:14:04 -04:00
}
2019-04-29 21:14:23 -04:00
if ( rules && rules . length ) {
if ( reasons && reasons . length )
reason _elements . push ( < div class = "tw-mg-y-05 tw-border-b" > < / div > ) ;
for ( const rule of rules ) {
reason _elements . push ( < li class = "tw-full-width tw-relative" >
< a
href = "#"
2021-02-11 19:40:12 -05:00
class = "tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive tw-pd-05"
2019-06-20 15:15:54 -04:00
onClick = { click _fn ( rule ) }
2019-04-29 21:14:23 -04:00
>
{ rule }
< / a >
< / li > ) ;
}
2019-04-29 18:14:04 -04:00
}
let reason _text ;
if ( data . definition . reason _text )
reason _text = data . definition . reason _text . call ( this , data , t , tip ) ;
else
reason _text = this . i18n . t ( 'chat.actions.select-reason' , 'Please select a reason from the list below:' ) ;
return ( < div class = "ffz--inline-reasons" >
{ reason _text ? < div class = "tw-pd-05 tw-border-b" >
{ reason _text }
< / div > : null }
2021-12-01 16:48:10 -05:00
< div class = "scrollable-area" data - simplebar >
< div class = "simplebar-scroll-content" >
< div class = "simplebar-content" >
< ul > { reason _elements } < / ul >
< / div >
< / div >
< / div >
2019-04-29 18:14:04 -04:00
< / div > ) ;
}
2018-04-28 17:56:03 -04:00
renderInlineContext ( target , data ) {
2019-08-06 15:39:45 -04:00
const definition = data . definition ;
let content ;
if ( definition . context )
content = ( t , tip ) => definition . context . call ( this , data , t , tip ) ;
else if ( definition . uses _reason ) {
content = ( t , tip ) => this . renderInlineReasons ( data , t , tip ) ;
} else
return ;
return this . renderPopup ( target , content ) ;
}
renderPopup ( target , content ) {
2018-04-28 17:56:03 -04:00
if ( target . _ffz _destroy )
return target . _ffz _destroy ( ) ;
const destroy = target . _ffz _destroy = ( ) => {
if ( target . _ffz _outside )
target . _ffz _outside . destroy ( ) ;
if ( target . _ffz _popup ) {
const fp = target . _ffz _popup ;
target . _ffz _popup = null ;
fp . destroy ( ) ;
}
2020-07-10 20:08:29 -04:00
if ( target . _ffz _on _destroy )
target . _ffz _on _destroy ( ) ;
target . _ffz _destroy = target . _ffz _outside = target . _ffz _on _destroy = null ;
2018-04-28 17:56:03 -04:00
}
2020-07-18 15:44:02 -04:00
const parent = document . fullscreenElement || document . body . querySelector ( '#root>div' ) || document . body ,
2018-04-28 17:56:03 -04:00
tt = target . _ffz _popup = new Tooltip ( parent , target , {
logger : this . log ,
manual : true ,
2019-04-29 18:14:04 -04:00
live : false ,
2018-04-28 17:56:03 -04:00
html : true ,
2019-06-12 21:13:53 -04:00
hover _events : true ,
2019-08-07 14:13:04 -04:00
no _update : true ,
2018-04-28 17:56:03 -04:00
2021-02-11 19:40:12 -05:00
tooltipClass : 'ffz-action-balloon ffz-balloon tw-block tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base' ,
arrowClass : 'ffz-balloon__tail tw-overflow-hidden tw-absolute' ,
arrowInner : 'ffz-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-c-background-base tw-absolute' ,
2019-04-29 18:14:04 -04:00
innerClass : '' ,
2018-04-28 17:56:03 -04:00
popper : {
placement : 'bottom' ,
modifiers : {
preventOverflow : {
boundariesElement : parent
} ,
flip : {
behavior : [ 'bottom' , 'top' , 'left' , 'right' ]
}
}
} ,
2019-04-29 18:14:04 -04:00
content ,
2019-06-12 21:13:53 -04:00
onShow : async ( t , tip ) => {
await tip . waitForDom ( ) ;
target . _ffz _outside = new ClickOutside ( tip . outer , destroy )
} ,
onMove : ( target , tip , event ) => {
this . emit ( 'tooltips:mousemove' , target , tip , event )
} ,
onLeave : ( target , tip , event ) => {
this . emit ( 'tooltips:leave' , target , tip , event ) ;
} ,
2018-04-28 17:56:03 -04:00
onHide : destroy
} ) ;
tt . _enter ( target ) ;
}
2019-04-18 21:07:11 -04:00
getUserLevel ( room , user ) { // eslint-disable-line class-methods-use-this
if ( ! room || ! user )
return 0 ;
if ( room . id === user . id || room . login === user . login )
return 5 ;
else if ( user . moderator || user . type === 'mod' || ( user . badges && user . badges . moderator ) )
return 3 ;
return 0 ;
}
2019-08-13 16:22:04 -04:00
renderRoom ( mod _icons , current _user , current _room , is _above , createElement ) {
2021-03-02 19:50:25 -05:00
const lines = [ ] ,
2019-05-07 15:04:12 -04:00
chat = this . resolve ( 'site.chat' ) ;
2021-03-02 19:50:25 -05:00
let line = null ;
2019-05-07 15:04:12 -04:00
for ( const data of this . parent . context . get ( 'chat.actions.room' ) ) {
2021-03-02 19:50:25 -05:00
if ( ! data )
continue ;
2022-12-07 16:52:07 -05:00
data . ctx = 'room' ;
2021-03-02 19:50:25 -05:00
const type = data . type ;
if ( type ) {
if ( type === 'new-line' ) {
line = null ;
} else if ( type === 'space' ) {
if ( ! line )
lines . push ( line = [ ] ) ;
line . push ( < div class = "tw-flex-grow-1" / > ) ;
} else if ( type === 'space-small' ) {
if ( ! line )
lines . push ( line = [ ] ) ;
line . push ( < div class = "tw-mg-x-1" / > ) ;
} else
this . log . warn ( 'Unknown action type' , type ) ;
continue ;
}
if ( ! data . action || ! data . appearance )
2019-05-07 15:04:12 -04:00
continue ;
2019-11-25 17:50:20 -05:00
let ap = data . appearance || { } ;
const disp = data . display || { } ,
act = this . actions [ data . action ] ;
2019-05-07 15:04:12 -04:00
2019-11-25 17:50:20 -05:00
if ( ! act || disp . disabled ||
2019-05-07 15:04:12 -04:00
( disp . mod _icons != null && disp . mod _icons !== ! ! mod _icons ) ||
( disp . mod != null && disp . mod !== ( current _user ? ! ! current _user . mod : false ) ) ||
2019-08-12 22:52:57 -04:00
( disp . staff != null && disp . staff !== ( current _user ? ! ! current _user . staff : false ) ) ||
( disp . emoteOnly != null && disp . emoteOnly !== current _room . emoteOnly ) ||
( disp . slowMode != null && disp . slowMode !== current _room . slowMode ) ||
2019-08-27 16:18:12 -04:00
( disp . subsMode != null && disp . subsMode !== current _room . subsMode ) ||
( disp . r9kMode != null && disp . r9kMode !== current _room . r9kMode ) ||
( disp . followersOnly != null && disp . followersOnly !== current _room . followersOnly ) )
2019-05-07 15:04:12 -04:00
continue ;
2020-08-12 16:10:06 -04:00
if ( maybe _call ( act . hidden , this , data , null , current _room , current _user , mod _icons ) )
continue ;
2022-12-07 16:52:07 -05:00
if ( ap . type === 'dynamic' ) {
const out = act . dynamicAppearance && act . dynamicAppearance . call ( this , Object . assign ( { } , ap ) , data , null , current _room , current _user , mod _icons ) ;
if ( out )
ap = out ;
}
2019-11-25 17:50:20 -05:00
if ( act . override _appearance ) {
const out = act . override _appearance . call ( this , Object . assign ( { } , ap ) , data , null , current _room , current _user , mod _icons ) ;
if ( out )
ap = out ;
}
const def = this . renderers [ ap . type ] ;
if ( ! def )
continue ;
2019-05-07 15:04:12 -04:00
const has _color = def . colored && ap . color ,
2019-11-25 17:50:20 -05:00
disabled = maybe _call ( act . disabled , this , data , null , current _room , current _user , mod _icons ) || false ,
2019-05-07 15:04:12 -04:00
color = has _color && ( chat && chat . colors ? chat . colors . process ( ap . color ) : ap . color ) ,
contents = def . render . call ( this , ap , createElement , color ) ;
2021-03-02 19:50:25 -05:00
if ( ! line )
lines . push ( line = [ ] ) ;
line . push ( < button
2021-03-02 16:55:25 -05:00
class = { ` ffz-tooltip tw-pd-x-05 mod-icon ffz-mod-icon tw-c-text-alt-2 ${ disabled ? ' disabled' : '' } ${ has _color ? ' colored' : '' } ` }
2019-05-07 15:04:12 -04:00
data - tooltip - type = "action"
data - action = { data . action }
data - options = { data . options ? JSON . stringify ( data . options ) : null }
data - tip = { ap . tooltip }
onClick = { this . handleClick }
onContextMenu = { this . handleContext }
>
{ contents }
< / button > ) ;
}
2021-03-02 19:50:25 -05:00
if ( ! lines . length )
2019-05-07 15:04:12 -04:00
return null ;
2021-03-02 19:50:25 -05:00
const room = current _room && JSON . stringify ( current _room ) ,
multi _line = lines . length > 1 ;
const actions = multi _line ?
lines . map ( ( line , idx ) => < div key = { idx } class = "tw-flex tw-full-width tw-flex-row tw-flex-wrap" > { line } < / div > ) :
lines [ 0 ] ;
2019-05-07 15:04:12 -04:00
return ( < div
2021-03-02 19:50:25 -05:00
class = { ` ffz--room-actions ${ multi _line ? ' tw-flex-column' : '' } ffz-action-data tw-flex tw-flex-grow-1 tw-flex-wrap tw-align-items-center ${ is _above ? 'tw-pd-y-05 tw-border-t' : 'tw-mg-x-05' } ` }
2019-05-07 15:04:12 -04:00
data - room = { room }
>
{ actions }
< / div > )
}
2019-08-06 15:39:45 -04:00
renderUserContext ( target , actions ) {
const fine = this . resolve ( 'site.fine' ) ,
site = this . resolve ( 'site' ) ,
chat = this . resolve ( 'site.chat' ) ,
line = fine && fine . searchParent ( target , n => n . props && n . props . message ) ;
const msg = line ? . props ? . message ;
if ( ! msg || ! site || ! chat )
return ;
let room = msg . roomLogin ? msg . roomLogin : msg . channel ? msg . channel . slice ( 1 ) : undefined ;
if ( ! room && line . props . channelID ) {
const r = this . parent . getRoom ( line . props . channelID , null , true ) ;
if ( r && r . login )
room = msg . roomLogin = r . login ;
}
const u = site . getUser ( ) ,
r = { id : line . props . channelID , login : room } ;
2022-12-08 16:19:14 -05:00
const has _replies = ! ! ( line . props . hasReply || line . props . reply || ! line . props . replyRestrictedReason ) ,
can _replies = has _replies && msg . message && ! msg . deleted && ! line . props . disableReplyClick ,
can _reply = can _replies && ( has _replies || ( u && u . login !== msg . user ? . login ) ) ;
2020-08-13 14:00:47 -04:00
2019-08-06 15:39:45 -04:00
msg . roomId = r . id ;
if ( u ) {
u . moderator = line . props . isCurrentUserModerator ;
u . staff = line . props . isCurrentUserStaff ;
2022-12-08 16:19:14 -05:00
u . reply _mode = this . parent . context . get ( 'chat.replies.style' ) ,
u . can _reply = can _reply ;
2019-08-06 15:39:45 -04:00
}
const current _level = this . getUserLevel ( r , u ) ,
msg _level = this . getUserLevel ( r , msg . user ) ;
let mod _icons = line . props . showModerationIcons ;
if ( current _level < 3 )
mod _icons = false ;
2020-08-12 18:34:40 -04:00
const chat _line = line ;
2019-08-06 15:39:45 -04:00
return this . renderPopup ( target , ( t , tip ) => {
const lines = [ ] ;
let line = null ;
const handle _click = event => {
2020-08-12 18:34:40 -04:00
this . handleClick ( event , line ) ;
2020-07-10 20:08:29 -04:00
tip . hide ( ) ;
2019-08-06 15:39:45 -04:00
} ;
for ( const data of actions ) {
if ( ! data )
continue ;
2022-12-07 16:52:07 -05:00
data . ctx = 'user_context' ;
2019-08-06 15:39:45 -04:00
if ( data . type === 'new-line' ) {
line = null ;
continue ;
} else if ( data . type === 'space-small' ) {
if ( ! line )
lines . push ( line = [ ] ) ;
line . push ( < div class = "tw-pd-x-1" / > ) ;
continue ;
} else if ( data . type === 'space' ) {
if ( ! line )
lines . push ( line = [ ] ) ;
line . push ( < div class = "tw-flex-grow-1" / > ) ;
continue ;
} else if ( ! data . action || ! data . appearance )
continue ;
2019-11-25 17:50:20 -05:00
let ap = data . appearance || { } ;
const disp = data . display || { } ,
act = this . actions [ data . action ] ;
2019-08-06 15:39:45 -04:00
2019-11-25 17:50:20 -05:00
if ( ! act || disp . disabled ||
2019-08-06 15:39:45 -04:00
( disp . mod _icons != null && disp . mod _icons !== ! ! mod _icons ) ||
( disp . mod != null && disp . mod !== ( current _level > msg _level ) ) ||
2019-08-09 14:24:26 -04:00
( disp . staff != null && disp . staff !== ( u ? ! ! u . staff : false ) ) ||
( disp . deleted != null && disp . deleted !== ! ! msg . deleted ) )
2019-08-06 15:39:45 -04:00
continue ;
2022-12-08 16:19:14 -05:00
if ( maybe _call ( act . hidden , this , data , msg , r , u , mod _icons , chat _line ) )
2020-08-12 16:10:06 -04:00
continue ;
2022-12-07 16:52:07 -05:00
if ( ap . type === 'dynamic' ) {
2022-12-08 16:19:14 -05:00
const out = act . dynamicAppearance && act . dynamicAppearance . call ( this , Object . assign ( { } , ap ) , data , msg , r , u , mod _icons , chat _line ) ;
2022-12-07 16:52:07 -05:00
if ( out )
ap = out ;
}
2019-11-25 17:50:20 -05:00
if ( act . override _appearance ) {
2022-12-08 16:19:14 -05:00
const out = act . override _appearance . call ( this , Object . assign ( { } , ap ) , data , msg , r , u , mod _icons , chat _line ) ;
2019-11-25 17:50:20 -05:00
if ( out )
ap = out ;
}
const def = this . renderers [ ap . type ] ;
if ( ! def )
continue ;
2019-08-06 15:39:45 -04:00
const has _color = def . colored && ap . color ,
2022-12-08 16:19:14 -05:00
disabled = maybe _call ( act . disabled , this , data , msg , r , u , mod _icons , chat _line ) || false ,
2019-08-06 15:39:45 -04:00
color = has _color && ( chat && chat . colors ? chat . colors . process ( ap . color ) : ap . color ) ,
contents = def . render . call ( this , ap , createElement , color ) ;
if ( ! line )
lines . push ( line = [ ] ) ;
const btn = ( < button
2019-11-25 17:50:20 -05:00
class = { ` ffz-tooltip ffz-tooltip--no-mouse tw-button tw-button--text ${ disabled ? ' tw-button--disabled disabled' : '' } ` }
2022-12-07 16:52:07 -05:00
//disabled={disabled}
2019-08-06 15:39:45 -04:00
data - tooltip - type = "action"
data - action = { data . action }
data - options = { data . options ? JSON . stringify ( data . options ) : null }
2019-08-09 14:24:26 -04:00
onClick = { handle _click } // eslint-disable-line react/jsx-no-bind
2019-08-06 15:39:45 -04:00
onContextMenu = { this . handleContext }
>
< span class = "tw-button__text" >
{ contents }
< / span >
< / button > ) ;
if ( ap . tooltip )
btn . dataset . tip = ap . tooltip ;
line . push ( btn ) ;
}
const out = ( < div class = "ffz-action-data tw-pd-05" data - source = "msg" >
< div class = "tw-pd-b-05 tw-border-b" >
< strong > { msg . user . displayName || msg . user . login } < / strong > ...
< / div >
{ lines . map ( line => {
if ( ! line || ! line . length )
return null ;
return ( < div class = "tw-flex tw-flex-no-wrap" >
{ line }
< / div > ) ;
} ) }
< / div > ) ;
out . ffz _message = msg ;
2020-08-12 18:34:40 -04:00
out . ffz _line = chat _line ;
2019-08-06 15:39:45 -04:00
return out ;
} ) ;
}
2022-12-07 16:52:07 -05:00
renderHover ( msg , mod _icons , current _user , current _room , createElement , instance = null ) {
const actions = [ ] ;
const current _level = this . getUserLevel ( current _room , current _user ) ,
msg _level = this . getUserLevel ( current _room , msg . user ) ,
is _self = msg . user && current _user && current _user . login === msg . user . login ;
if ( current _level < 3 )
mod _icons = false ;
const chat = this . resolve ( 'site.chat' ) ;
let had _action = false ;
for ( const data of this . parent . context . get ( 'chat.actions.hover' ) ) {
if ( ! data . action || ! data . appearance )
continue ;
data . ctx = 'hover' ;
let ap = data . appearance || { } ;
const disp = data . display || { } ,
keys = disp . keys ,
act = this . actions [ data . action ] ;
if ( ! act || disp . disabled ||
( disp . mod _icons != null && disp . mod _icons !== ! ! mod _icons ) ||
( disp . mod != null && disp . mod !== ( current _level > msg _level ) ) ||
( disp . staff != null && disp . staff !== ( current _user ? ! ! current _user . staff : false ) ) ||
( disp . deleted != null && disp . deleted !== ! ! msg . deleted ) )
continue ;
if ( is _self && ! act . can _self )
continue ;
if ( maybe _call ( act . hidden , this , data , msg , current _room , current _user , mod _icons , instance ) )
continue ;
if ( ap . type === 'dynamic' ) {
const out = act . dynamicAppearance && act . dynamicAppearance . call ( this , Object . assign ( { } , ap ) , data , msg , current _room , current _user , mod _icons , instance ) ;
if ( out )
ap = out ;
}
if ( act . override _appearance ) {
const out = act . override _appearance . call ( this , Object . assign ( { } , ap ) , data , msg , current _room , current _user , mod _icons , instance ) ;
if ( out )
ap = out ;
}
const def = this . renderers [ ap . type ] ;
if ( ! def )
continue ;
const has _color = def . colored && ap . color ,
disabled = maybe _call ( act . disabled , this , data , msg , current _room , current _user , mod _icons , instance ) || false ,
color = has _color && ( chat && chat . colors ? chat . colors . process ( ap . color ) : ap . color ) ,
contents = def . render . call ( this , ap , createElement , color ) ;
had _action = true ;
actions . push ( < div class = { ` ffz-hover-action ${ keys ? ` ffz-has-modifier ffz-modifier- ${ keys } ` : '' } ` } >
< button
class = { ` ffz-tooltip ffz-mod-icon tw-c-text-alt-2 ${ disabled ? ' disabled' : '' } ${ has _color ? ' colored' : '' } ` }
//disabled={disabled}
data - tooltip - type = "action"
data - action = { data . action }
data - options = { data . options ? JSON . stringify ( data . options ) : null }
data - tip = { ap . tooltip }
onClick = { this . handleClick }
onContextMenu = { this . handleContext }
>
{ contents }
< / button >
< / div > ) ;
}
if ( ! had _action )
return null ;
return ( < div
class = { ` ffz--hover-actions ffz-action-data tw-mg-r-05 ` }
data - source = "line"
>
{ actions }
< / div > ) ;
}
2022-10-14 17:23:45 -04:00
renderInline ( msg , mod _icons , current _user , current _room , createElement , instance = null ) {
2018-04-28 17:56:03 -04:00
const actions = [ ] ;
2019-04-18 21:07:11 -04:00
const current _level = this . getUserLevel ( current _room , current _user ) ,
2020-08-12 16:10:06 -04:00
msg _level = this . getUserLevel ( current _room , msg . user ) ,
is _self = msg . user && current _user && current _user . login === msg . user . login ;
2019-04-18 21:07:11 -04:00
2019-05-08 15:39:14 -04:00
if ( current _level < 3 )
mod _icons = false ;
2019-05-16 14:46:26 -04:00
const chat = this . resolve ( 'site.chat' ) ,
modified = [ ] ;
let had _action = false ;
2018-04-28 17:56:03 -04:00
for ( const data of this . parent . context . get ( 'chat.actions.inline' ) ) {
if ( ! data . action || ! data . appearance )
continue ;
2022-12-07 16:52:07 -05:00
data . ctx = 'inline' ;
2019-11-25 17:50:20 -05:00
let ap = data . appearance || { } ;
const disp = data . display || { } ,
2019-05-16 14:46:26 -04:00
keys = disp . keys ,
2020-08-13 14:00:47 -04:00
hover = disp . hover ,
2019-11-25 17:50:20 -05:00
act = this . actions [ data . action ] ;
2018-04-28 17:56:03 -04:00
2019-11-25 17:50:20 -05:00
if ( ! act || disp . disabled ||
2018-05-10 19:56:39 -04:00
( disp . mod _icons != null && disp . mod _icons !== ! ! mod _icons ) ||
2019-04-18 21:07:11 -04:00
( disp . mod != null && disp . mod !== ( current _level > msg _level ) ) ||
2018-05-10 19:56:39 -04:00
( disp . staff != null && disp . staff !== ( current _user ? ! ! current _user . staff : false ) ) ||
( disp . deleted != null && disp . deleted !== ! ! msg . deleted ) )
2018-04-28 17:56:03 -04:00
continue ;
2020-08-12 16:10:06 -04:00
if ( is _self && ! act . can _self )
continue ;
2022-10-14 17:23:45 -04:00
if ( maybe _call ( act . hidden , this , data , msg , current _room , current _user , mod _icons , instance ) )
2020-08-12 16:10:06 -04:00
continue ;
2022-12-07 16:52:07 -05:00
if ( ap . type === 'dynamic' ) {
const out = act . dynamicAppearance && act . dynamicAppearance . call ( this , Object . assign ( { } , ap ) , data , msg , current _room , current _user , mod _icons , instance ) ;
if ( out )
ap = out ;
}
2019-11-25 17:50:20 -05:00
if ( act . override _appearance ) {
2022-10-14 17:23:45 -04:00
const out = act . override _appearance . call ( this , Object . assign ( { } , ap ) , data , msg , current _room , current _user , mod _icons , instance ) ;
2019-11-25 17:50:20 -05:00
if ( out )
ap = out ;
}
const def = this . renderers [ ap . type ] ;
if ( ! def )
continue ;
2018-04-28 17:56:03 -04:00
const has _color = def . colored && ap . color ,
2022-10-14 17:23:45 -04:00
disabled = maybe _call ( act . disabled , this , data , msg , current _room , current _user , mod _icons , instance ) || false ,
2018-04-28 17:56:03 -04:00
color = has _color && ( chat && chat . colors ? chat . colors . process ( ap . color ) : ap . color ) ,
contents = def . render . call ( this , ap , createElement , color ) ;
2019-05-16 14:46:26 -04:00
let list = actions ;
2020-08-13 14:00:47 -04:00
if ( keys || hover )
2019-05-16 14:46:26 -04:00
list = modified ;
had _action = true ;
list . push ( < button
2021-03-02 16:55:25 -05:00
class = { ` ffz-tooltip mod-icon ffz-mod-icon tw-c-text-alt-2 ${ disabled ? ' disabled' : '' } ${ has _color ? ' colored' : '' } ${ keys ? ` ffz-modifier- ${ keys } ` : '' } ${ hover ? ' ffz-hover' : '' } ` }
2022-12-07 16:52:07 -05:00
//disabled={disabled}
2018-04-28 17:56:03 -04:00
data - tooltip - type = "action"
data - action = { data . action }
data - options = { data . options ? JSON . stringify ( data . options ) : null }
data - tip = { ap . tooltip }
onClick = { this . handleClick }
onContextMenu = { this . handleContext }
>
{ contents }
< / button > ) ;
}
2019-05-16 14:46:26 -04:00
if ( ! had _action )
2018-04-28 17:56:03 -04:00
return null ;
2019-05-16 14:51:51 -04:00
let out = null ;
if ( actions . length )
out = ( < div
class = "ffz--inline-actions ffz-action-data tw-inline-block tw-mg-r-05"
data - source = "line"
>
{ actions }
< / div > ) ;
2019-05-16 14:46:26 -04:00
if ( modified . length ) {
2019-05-16 14:51:51 -04:00
const modified _out = ( < div
class = "ffz--inline-actions ffz--modifier-actions ffz-action-data"
data - source = "line"
>
{ modified }
< / div > ) ;
if ( out )
return [ out , modified _out ] ;
return modified _out ;
2019-05-16 14:46:26 -04:00
}
return out ;
2018-04-28 17:56:03 -04:00
}
getData ( element ) {
const ds = element . dataset ,
2018-07-13 14:32:12 -04:00
parent = element . closest ( '.ffz-action-data' ) ,
2018-04-28 17:56:03 -04:00
pds = parent && parent . dataset ,
action = ds && ds . action ,
definition = this . actions [ action ] ;
if ( ! definition )
return null ;
2020-08-12 18:34:40 -04:00
let user , room , message , loaded = false , line ;
2019-05-07 15:04:12 -04:00
if ( pds ) {
2019-08-06 15:39:45 -04:00
if ( pds . source === 'msg' && parent . ffz _message ) {
const msg = parent . ffz _message ;
2020-08-12 18:34:40 -04:00
line = parent . ffz _line ;
2019-08-06 15:39:45 -04:00
loaded = true ;
user = msg . user ? {
color : msg . user . color ,
id : msg . user . id ,
login : msg . user . login ,
displayName : msg . user . displayName ,
type : msg . user . type
} : null ;
room = {
login : msg . roomLogin ,
id : msg . roomId
} ;
message = {
id : msg . id ,
text : msg . message
} ;
} else if ( pds . source === 'line' ) {
2020-08-12 18:34:40 -04:00
const fine = this . resolve ( 'site.fine' ) ;
line = fine && fine . searchParent ( parent , n => n . props && n . props . message ) ;
2019-05-07 15:04:12 -04:00
if ( line && line . props && line . props . message ) {
loaded = true ;
const msg = line . props . message ;
user = msg . user ? {
color : msg . user . color ,
id : msg . user . id ,
login : msg . user . login ,
displayName : msg . user . displayName ,
type : msg . user . type
} : null ;
room = {
login : line . props . channelLogin ,
id : line . props . channelID
}
message = {
id : msg . id ,
text : msg . message
}
}
}
if ( ! loaded ) {
user = pds . user ? JSON . parse ( pds . user ) : null ;
room = pds . room ? JSON . parse ( pds . room ) : null ;
message = pds . message ? JSON . parse ( pds . message ) : pds . msgId ? { id : pds . msgId } : null ;
}
}
const data = {
action ,
definition ,
tip : ds . tip ,
options : ds . options ? JSON . parse ( ds . options ) : null ,
user ,
room ,
message ,
2020-08-12 18:34:40 -04:00
message _id : message ? message . id : null ,
line
2019-05-07 15:04:12 -04:00
} ;
2018-04-28 17:56:03 -04:00
if ( definition . defaults )
data . options = Object . assign ( { } , maybe _call ( definition . defaults , this , data , element ) , data . options ) ;
return data ;
}
handleClick ( event ) {
const target = event . target ,
data = this . getData ( target ) ;
if ( ! data )
return ;
2019-11-25 17:50:20 -05:00
if ( target . classList . contains ( 'disabled' ) )
return ;
2018-04-28 17:56:03 -04:00
if ( ! data . definition . click ) {
if ( data . definition . context )
return this . handleContext ( event ) ;
return this . log . warn ( ` No click handler for action provider " ${ data . action } " ` ) ;
}
2020-07-18 15:44:02 -04:00
if ( target . _ffz _tooltip )
target . _ffz _tooltip . hide ( ) ;
2018-04-28 17:56:03 -04:00
return data . definition . click . call ( this , event , data ) ;
}
handleContext ( event ) {
if ( event . shiftKey )
return ;
event . preventDefault ( ) ;
const target = event . target ,
data = this . getData ( event . target ) ;
if ( ! data )
return ;
2019-11-25 17:50:20 -05:00
if ( target . classList . contains ( 'disabled' ) )
return ;
2020-07-18 15:44:02 -04:00
if ( target . _ffz _tooltip )
target . _ffz _tooltip . hide ( ) ;
2018-04-28 17:56:03 -04:00
2019-04-29 18:14:04 -04:00
if ( ! data . definition . context && ! data . definition . uses _reason )
return ;
2019-08-06 15:39:45 -04:00
this . renderInlineContext ( target , data ) ;
2018-04-28 17:56:03 -04:00
}
2019-08-06 15:39:45 -04:00
handleUserContext ( event ) {
if ( event . shiftKey )
return ;
const actions = this . parent . context . get ( 'chat.actions.user-context' ) ;
if ( ! Array . isArray ( actions ) || ! actions . length )
return ;
event . preventDefault ( ) ;
const target = event . target ;
2020-07-18 15:44:02 -04:00
if ( target . _ffz _tooltip )
target . _ffz _tooltip . hide ( ) ;
2019-08-06 15:39:45 -04:00
this . renderUserContext ( target , actions ) ;
}
2018-04-28 17:56:03 -04:00
2019-06-27 23:19:05 -04:00
pasteMessage ( room , message ) {
return this . resolve ( 'site.chat.input' ) . pasteMessage ( room , message ) ;
}
2018-04-28 17:56:03 -04:00
sendMessage ( room , message ) {
return this . resolve ( 'site.chat' ) . sendMessage ( room , message ) ;
}
}