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' ;
import { ClickOutside } from 'utilities/dom' ;
import Tooltip from 'utilities/tooltip' ;
import * as ACTIONS from './types' ;
import * as RENDERERS from './renderers' ;
export default class Actions extends Module {
constructor ( ... args ) {
super ( ... args ) ;
this . inject ( 'settings' ) ;
this . inject ( 'tooltips' ) ;
this . inject ( 'i18n' ) ;
this . actions = { } ;
this . renderers = { } ;
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 } } } ,
] ,
type : 'array_merge' ,
ui : {
2018-07-05 20:27:17 -04:00
path : 'Chat > In-Line Actions @{"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-07-19 22:03:01 -04:00
context : [ 'user' , 'room' ] ,
2018-04-28 17:56:03 -04:00
inline : 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 )
}
}
}
} ) ;
2018-07-13 14:32:12 -04:00
this . settings . add ( 'chat.actions.viewer-card' , {
// Filter out actions
process : ( ctx , val ) =>
2018-07-19 22:03:01 -04:00
val . filter ( x => x . type || ( x . appearance &&
2018-07-13 14:32:12 -04:00
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 ] )
2018-07-19 22:03:01 -04:00
) ) ,
2018-07-13 14:32:12 -04:00
default : [
{ v : { action : 'friend' } } ,
{ v : { action : 'whisper' , appearance : { type : 'text' , text : 'Whisper' , button : true } } } ,
{ v : { type : 'space' } } ,
{ v : { action : 'card_menu' } } ,
{ v : { type : 'new-line' } } ,
{ v : { action : 'ban' , appearance : { type : 'icon' , icon : 'ffz-i-block' } , display : { mod : true } } } ,
{ v : { action : 'timeout' , appearance : { type : 'icon' , icon : 'ffz-i-clock' } , display : { mod : true } } }
] ,
type : 'array_merge' ,
_ui : {
path : 'Chat > Viewer Cards >> tabs ~> Actions @{"description": "Here, you define what actions are available on viewer cards."}' ,
component : 'chat-actions' ,
2018-07-19 22:03:01 -04:00
context : [ 'user' , 'room' , 'product' ] ,
2018-07-13 14:32: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 )
}
}
}
} )
2018-04-28 17:56:03 -04:00
this . handleClick = this . handleClick . bind ( this ) ;
this . handleContext = this . handleContext . bind ( this ) ;
}
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 )
return data . tip ;
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 ;
for ( const ctx of this . settings . _ _contexts )
ctx . update ( 'chat.actions.inline' ) ;
}
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 ;
for ( const ctx of this . settings . _ _contexts )
ctx . update ( 'chat.actions.inline' ) ;
}
renderInlineContext ( target , data ) {
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 ( ) ;
}
target . _ffz _destroy = target . _ffz _outside = null ;
}
const parent = document . body . querySelector ( '.twilight-root,.twilight-minimal-root' ) || document . body ,
tt = target . _ffz _popup = new Tooltip ( parent , target , {
logger : this . log ,
manual : true ,
html : true ,
2018-09-25 18:37:14 -04:00
tooltipClass : 'ffz-action-balloon tw-balloon tw-block tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base' ,
2018-04-28 17:56:03 -04:00
arrowClass : 'tw-balloon__tail tw-overflow-hidden tw-absolute' ,
2018-09-25 18:37:14 -04:00
arrowInner : 'tw-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' ,
2018-04-28 17:56:03 -04:00
innerClass : 'tw-pd-1' ,
popper : {
placement : 'bottom' ,
modifiers : {
preventOverflow : {
boundariesElement : parent
} ,
flip : {
behavior : [ 'bottom' , 'top' , 'left' , 'right' ]
}
}
} ,
content : ( t , tip ) => data . definition . context . call ( this , data , t , tip ) ,
onShow : ( t , tip ) =>
setTimeout ( ( ) => {
target . _ffz _outside = new ClickOutside ( tip . outer , destroy )
} ) ,
onHide : destroy
} ) ;
tt . _enter ( target ) ;
}
renderInline ( msg , mod _icons , current _user , current _room , createElement ) {
const actions = [ ] ;
2018-05-18 02:10:00 -04:00
if ( msg . user && current _user && current _user . login === msg . user . login )
2018-04-28 17:56:03 -04:00
return ;
const chat = this . resolve ( 'site.chat' ) ;
for ( const data of this . parent . context . get ( 'chat.actions.inline' ) ) {
if ( ! data . action || ! data . appearance )
continue ;
const ap = data . appearance || { } ,
disp = data . display || { } ,
def = this . renderers [ ap . type ] ;
if ( ! def || disp . disabled ||
2018-05-10 19:56:39 -04:00
( disp . mod _icons != null && disp . mod _icons !== ! ! mod _icons ) ||
( disp . mod != null && disp . mod !== ( current _user ? ! ! current _user . moderator : false ) ) ||
( 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 ;
const has _color = def . colored && ap . color ,
color = has _color && ( chat && chat . colors ? chat . colors . process ( ap . color ) : ap . color ) ,
contents = def . render . call ( this , ap , createElement , color ) ;
actions . push ( < button
class = { ` ffz-tooltip ffz-mod-icon mod-icon tw-c-text-alt-2 ${ has _color ? ' colored' : '' } ` }
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 > ) ;
}
if ( ! actions . length )
return null ;
const room = current _room && JSON . stringify ( current _room ) ,
user = msg . user && JSON . stringify ( {
2018-05-18 02:10:00 -04:00
login : msg . user . login ,
displayName : msg . user . displayName ,
id : msg . user . id ,
type : msg . user . type
2018-04-28 17:56:03 -04:00
} ) ;
return ( < div
2018-07-16 13:57:56 -04:00
class = "ffz--inline-actions ffz-action-data tw-inline-block tw-mg-r-05"
2018-04-28 17:56:03 -04:00
data - msg - id = { msg . id }
data - user = { user }
data - room = { room }
>
{ actions }
< / div > ) ;
}
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 ;
const user = pds && pds . user ? JSON . parse ( pds . user ) : null ,
room = pds && pds . room ? JSON . parse ( pds . room ) : null ,
message _id = pds && pds . msgId ,
data = {
action ,
definition ,
tip : ds . tip ,
options : ds . options ? JSON . parse ( ds . options ) : null ,
user ,
room ,
message _id
} ;
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 ;
if ( ! data . definition . click ) {
if ( data . definition . context )
return this . handleContext ( event ) ;
return this . log . warn ( ` No click handler for action provider " ${ data . action } " ` ) ;
}
if ( target . _ffz _tooltip$0 )
target . _ffz _tooltip$0 . hide ( ) ;
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 ;
if ( ! data . definition . context )
return ;
if ( target . _ffz _tooltip$0 )
target . _ffz _tooltip$0 . hide ( ) ;
this . renderInlineContext ( event . target , data ) ;
}
sendMessage ( room , message ) {
return this . resolve ( 'site.chat' ) . sendMessage ( room , message ) ;
}
}