import React , { Component } from "react" ; import { PubSubClient } from "flok-core" ; import TargetMessagesPane , { Message } from "./TargetMessagesPane" ; import SessionClient , { IceServerType } from "../lib/SessionClient" ; import Mosaic from "./Mosaic" ; import Audio from "./Audio" ; const MAX_LINES = 100 ; const LOCAL_TARGETS = [ "hydra" ] ; type Props = { websocketsHost : string ; sessionName : string ; userName ? : string ; onHydraEvaluation : ( code : string ) => void ; audioStreamingEnabled : boolean ; readonly : boolean ; noLocalEval : boolean ; extraIceServers ? : IceServerType [ ] ; layout : { editors : { id : string ; target : string ; } [ ] ; } ; } ; type State = { showTargetMessagesPane : boolean ; showTextEditors : boolean ; messagesByClientId : { [ clientId ] : Message [ ] ; } ; messagesPaneIsTop : boolean ; messagesPaneIsMaximized : boolean ; } ; class Session extends Component < Props , State > { state : State = { showTargetMessagesPane : false , showTextEditors : false , messagesByClientId : { } , messagesPaneIsTop : false , messagesPaneIsMaximized : false , } ; pubsubClient : PubSubClient ; sessionClient : SessionClient ; static defaultProps = { userName : "anonymous" , onHydraEvaluation : ( ) => { } , readonly : false , } ; componentDidMount ( ) { const { sessionName , userName , layout , extraIceServers } = this . props ; const targets = [ ... new Set ( layout . editors . map ( ( { target } ) => target ) ) ] ; console . log ( "Targets:" , targets ) ; const wsUrl = this . getWebsocketsUrl ( ) ; const signalingServerUrl = `${ wsUrl }/signal` ; console . log ( `Signaling server URL: ${ signalingServerUrl }` ) ; const websocketServerUrl = `${ wsUrl }/doc` ; console . log ( `WebSocket server URL: ${ websocketServerUrl }` ) ; const pubsubUrl = `${ wsUrl }/pubsub` ; console . log ( `Pub/Sub server URL: ${ pubsubUrl }` ) ; this . sessionClient = new SessionClient ( { signalingServerUrl , websocketServerUrl , extraIceServers , sessionName , userName , onJoin : ( ) => { this . sessionClient . setUsername ( userName ) ; this . setState ( { showTextEditors : true } ) ; } , onInitialSync : ( method , editors ) => { this . postInitialSessionContentToParentWindow ( method , editors ) ; } , } ) ; this . sessionClient . join ( ) ; this . pubsubClient = new PubSubClient ( pubsubUrl , { connect : true , reconnect : true , } ) ; targets . forEach ( ( target ) => { this . pubsubClient . subscribe ( `session:${ sessionName }:target:${ target }:eval` , ( content ) => this . handleEvaluateRemoteCode ( { target , content } ) ) ; this . pubsubClient . subscribe ( `session:${ sessionName }:target:${ target }:out` , ( content ) => this . handleMessageTarget ( { target , content } ) ) ; this . pubsubClient . subscribe ( `session:${ sessionName }:target:${ target }:user:${ userName }:out` , ( content ) => this . handleMessageTarget ( { target , content } ) ) ; } ) ; } postInitialSessionContentToParentWindow ( method , editors ) { console . log ( "Initial sync from" , method ) ; this . postMessageToParentWindow ( { cmd : "initialSync" , args : { method , editors } , } ) ; } componentDidUpdate ( prevProps , _prevState ) { if ( this . sessionClient ) { const { userName } = this . props ; if ( prevProps . userName !== userName ) { console . log ( `Change username to '${ userName }'` ) ; this . sessionClient . setUsername ( userName ) ; } } } componentWillUnmount ( ) { if ( this . sessionClient ) { this . sessionClient . release ( ) ; this . sessionClient = null ; } if ( this . pubsubClient ) { this . pubsubClient . disconnect ( ) ; this . pubsubClient = null ; } } getWebsocketsUrl ( ) { const { websocketsHost } = this . props ; const protocol = location . protocol === "https:" ? "wss:" : "ws:" ; return `${ protocol }//${ websocketsHost }` ; } evaluateLocalCode ( { target , body } ) { const { noLocalEval } = this . props ; if ( noLocalEval ) return ; switch ( target ) { case "hydra" : const { onHydraEvaluation } = this . props ; onHydraEvaluation && onHydraEvaluation ( body ) ; break ; default : console . error ( "Unhandle local target:" , target ) ; } } handleEvaluateCode = ( { editorId , target , body , fromLine , toLine , user , locally = false , } ) => { const { sessionName } = this . props ; const { pubsubClient } = this ; const content = { editorId , fromLine , toLine , user , } ; this . setState ( { messagesByClientId : { } , showTargetMessagesPane : false } ) ; if ( LOCAL_TARGETS . includes ( target ) ) { this . evaluateLocalCode ( { target , body } ) ; if ( locally == false ) { pubsubClient . publish ( `session:${ sessionName }:target:${ target }:eval` , { body , ... content , } ) ; } } else { if ( locally === false ) { pubsubClient . publish ( `session:${ sessionName }:target:${ target }:eval` , content ) ; } } pubsubClient . publish ( `session:${ sessionName }:target:${ target }:in` , { user , body , } ) ; this . postMessageToParentWindow ( { cmd : "evaluateCode" , args : { editorId , target , body , user , local : locally } , } ) ; } ; handleEvaluateRemoteCode = ( { target , content } ) => { const { editorId , user , fromLine , toLine } = content ; this . setState ( { showTargetMessagesPane : false } ) ; if ( LOCAL_TARGETS . includes ( target ) ) { const { body } = content ; this . evaluateLocalCode ( { target , body } ) ; } if ( this . props . userName !== user ) { this . sessionClient . flash ( editorId , fromLine , toLine ) ; } const { body } = content ; this . postMessageToParentWindow ( { cmd : "evaluateCode" , args : { editorId , target , body , user , local : false } , } ) ; } ; postMessageToParentWindow ( payload ) { console . debug ( "Post message to parent window:" , payload ) ; window . parent . postMessage ( payload , "*" ) ; } handleMessageTarget = ( { target , content } ) => { console . debug ( `[message] [target=${ target }] ${ JSON . stringify ( content ) }` ) ; this . setState ( ( prevState ) => { const clientId = content . clientId || "default" ; const prevMessages = prevState . messagesByClientId [ clientId ] || [ ] ; const allMessages = [ ... prevMessages , { target , content } ] ; return { messagesByClientId : { ... prevState . messagesByClientId , [ clientId ] : allMessages . slice ( - MAX_LINES , allMessages . length ) , } , showTargetMessagesPane : true , } ; } ) ; } ; handleTargetMessagesPaneTogglePosition = ( ) => { this . setState ( ( prevState ) => ( { messagesPaneIsTop : ! prevState . messagesPaneIsTop , } ) ) ; } ; handleTargetMessagesPaneToggleMaximize = ( ) => { this . setState ( ( prevState ) => ( { messagesPaneIsMaximized : ! prevState . messagesPaneIsMaximized , } ) ) ; } ; handleTargetMessagesPaneClose = ( ) => { this . setState ( { showTargetMessagesPane : false } ) ; } ; render ( ) { const { messagesByClientId , showTextEditors , showTargetMessagesPane , messagesPaneIsTop , messagesPaneIsMaximized , } = this . state ; const { layout , audioStreamingEnabled , readonly } = this . props ; const { sessionClient } = this ; return ( < div className = "container" > { showTextEditors && ( < Mosaic layout = { layout } sessionClient = { sessionClient } onEvaluateCode = { this . handleEvaluateCode } readonly = { readonly } / > ) } { showTargetMessagesPane && messagesByClientId && ( < TargetMessagesPane messagesByClientId = { messagesByClientId } isTop = { messagesPaneIsTop } isMaximized = { messagesPaneIsMaximized } onTogglePosition = { this . handleTargetMessagesPaneTogglePosition } onToggleMaximize = { this . handleTargetMessagesPaneToggleMaximize } onClose = { this . handleTargetMessagesPaneClose } / > ) } { audioStreamingEnabled && < Audio sessionClient = { this . sessionClient } />} < style jsx > { `
          .container {
            height: 100vh;
          }
        ` } < /style> < /div>); } } export default Session ;