Merge branch 'v3.0.x-release' into avoid-unrelated-notifications
This commit is contained in:
commit
85f7110a7b
@ -75,5 +75,6 @@ daemonUser in Linux := user
|
||||
daemonGroup in Linux := group
|
||||
|
||||
javaOptions in Universal ++= Seq("-J-Xms130m", "-J-Xmx256m", "-Dconfig.file=/etc/bigbluebutton/bbb-apps-akka.conf", "-Dlogback.configurationFile=conf/logback.xml")
|
||||
javaOptions in reStart ++= Seq("-Dconfig.file=/etc/bigbluebutton/bbb-apps-akka.conf", "-Dlogback.configurationFile=conf/logback.xml")
|
||||
|
||||
debianPackageDependencies in Debian ++= Seq("java17-runtime-headless", "bash")
|
||||
|
@ -0,0 +1,25 @@
|
||||
package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.RightsManagementTrait
|
||||
import org.bigbluebutton.core.db.UserConnectionStatusDAO
|
||||
import org.bigbluebutton.core.models.{ UserState, Users2x }
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
|
||||
trait UserConnectionAliveReqMsgHdlr extends RightsManagementTrait {
|
||||
this: UsersApp =>
|
||||
|
||||
val liveMeeting: LiveMeeting
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleUserConnectionAliveReqMsg(msg: UserConnectionAliveReqMsg): Unit = {
|
||||
log.info("handleUserConnectionAliveReqMsg: userId={}", msg.body.userId)
|
||||
|
||||
for {
|
||||
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
|
||||
} yield {
|
||||
UserConnectionStatusDAO.updateUserAlive(user.intId)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.RightsManagementTrait
|
||||
import org.bigbluebutton.core.db.UserConnectionStatusDAO
|
||||
import org.bigbluebutton.core.models.{ UserState, Users2x }
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
|
||||
trait UserConnectionUpdateRttReqMsgHdlr extends RightsManagementTrait {
|
||||
this: UsersApp =>
|
||||
|
||||
val liveMeeting: LiveMeeting
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleUserConnectionUpdateRttReqMsg(msg: UserConnectionUpdateRttReqMsg): Unit = {
|
||||
log.info("handleUserConnectionUpdateRttReqMsg: networkRttInMs={} userId={}", msg.body.networkRttInMs, msg.body.userId)
|
||||
|
||||
for {
|
||||
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
|
||||
} yield {
|
||||
UserConnectionStatusDAO.updateUserRtt(user.intId, msg.body.networkRttInMs)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -168,6 +168,8 @@ class UsersApp(
|
||||
with AssignPresenterReqMsgHdlr
|
||||
with ChangeUserPinStateReqMsgHdlr
|
||||
with ChangeUserMobileFlagReqMsgHdlr
|
||||
with UserConnectionAliveReqMsgHdlr
|
||||
with UserConnectionUpdateRttReqMsgHdlr
|
||||
with ChangeUserReactionEmojiReqMsgHdlr
|
||||
with ChangeUserRaiseHandReqMsgHdlr
|
||||
with ChangeUserAwayReqMsgHdlr
|
||||
|
@ -5,18 +5,22 @@ import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{ Failure, Success }
|
||||
|
||||
case class UserConnectionStatusDbModel(
|
||||
userId: String,
|
||||
meetingId: String,
|
||||
connectionAliveAt: Option[java.sql.Timestamp]
|
||||
userId: String,
|
||||
meetingId: String,
|
||||
connectionAliveAt: Option[java.sql.Timestamp],
|
||||
userClientResponseAt: Option[java.sql.Timestamp],
|
||||
networkRttInMs: Option[Double]
|
||||
)
|
||||
|
||||
class UserConnectionStatusDbTableDef(tag: Tag) extends Table[UserConnectionStatusDbModel](tag, None, "user_connectionStatus") {
|
||||
override def * = (
|
||||
userId, meetingId, connectionAliveAt
|
||||
userId, meetingId, connectionAliveAt, userClientResponseAt, networkRttInMs
|
||||
) <> (UserConnectionStatusDbModel.tupled, UserConnectionStatusDbModel.unapply)
|
||||
val userId = column[String]("userId", O.PrimaryKey)
|
||||
val meetingId = column[String]("meetingId")
|
||||
val connectionAliveAt = column[Option[java.sql.Timestamp]]("connectionAliveAt")
|
||||
val userClientResponseAt = column[Option[java.sql.Timestamp]]("userClientResponseAt")
|
||||
val networkRttInMs = column[Option[Double]]("networkRttInMs")
|
||||
}
|
||||
|
||||
object UserConnectionStatusDAO {
|
||||
@ -27,7 +31,9 @@ object UserConnectionStatusDAO {
|
||||
UserConnectionStatusDbModel(
|
||||
userId = userId,
|
||||
meetingId = meetingId,
|
||||
connectionAliveAt = None
|
||||
connectionAliveAt = None,
|
||||
userClientResponseAt = None,
|
||||
networkRttInMs = None
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
@ -36,4 +42,28 @@ object UserConnectionStatusDAO {
|
||||
}
|
||||
}
|
||||
|
||||
def updateUserAlive(userId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserConnectionStatusDbTableDef]
|
||||
.filter(_.userId === userId)
|
||||
.map(t => (t.connectionAliveAt))
|
||||
.update(Some(new java.sql.Timestamp(System.currentTimeMillis())))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated connectionAliveAt on UserConnectionStatus table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating connectionAliveAt on UserConnectionStatus: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def updateUserRtt(userId: String, networkRttInMs: Double) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserConnectionStatusDbTableDef]
|
||||
.filter(_.userId === userId)
|
||||
.map(t => (t.networkRttInMs, t.userClientResponseAt))
|
||||
.update((Some(networkRttInMs), Some(new java.sql.Timestamp(System.currentTimeMillis()))))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated networkRttInMs on UserConnectionStatus table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating networkRttInMs on UserConnectionStatus: $e")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -111,6 +111,10 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeGenericMsg[ChangeUserPinStateReqMsg](envelope, jsonNode)
|
||||
case ChangeUserMobileFlagReqMsg.NAME =>
|
||||
routeGenericMsg[ChangeUserMobileFlagReqMsg](envelope, jsonNode)
|
||||
case UserConnectionAliveReqMsg.NAME =>
|
||||
routeGenericMsg[UserConnectionAliveReqMsg](envelope, jsonNode)
|
||||
case UserConnectionUpdateRttReqMsg.NAME =>
|
||||
routeGenericMsg[UserConnectionUpdateRttReqMsg](envelope, jsonNode)
|
||||
case SetUserSpeechLocaleReqMsg.NAME =>
|
||||
routeGenericMsg[SetUserSpeechLocaleReqMsg](envelope, jsonNode)
|
||||
case SelectRandomViewerReqMsg.NAME =>
|
||||
|
@ -405,6 +405,8 @@ class MeetingActor(
|
||||
case m: SelectRandomViewerReqMsg => usersApp.handleSelectRandomViewerReqMsg(m)
|
||||
case m: ChangeUserPinStateReqMsg => usersApp.handleChangeUserPinStateReqMsg(m)
|
||||
case m: ChangeUserMobileFlagReqMsg => usersApp.handleChangeUserMobileFlagReqMsg(m)
|
||||
case m: UserConnectionAliveReqMsg => usersApp.handleUserConnectionAliveReqMsg(m)
|
||||
case m: UserConnectionUpdateRttReqMsg => usersApp.handleUserConnectionUpdateRttReqMsg(m)
|
||||
case m: SetUserSpeechLocaleReqMsg => usersApp.handleSetUserSpeechLocaleReqMsg(m)
|
||||
|
||||
// Client requested to eject user
|
||||
|
@ -78,6 +78,8 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
|
||||
case m: AssignPresenterReqMsg => logMessage(msg)
|
||||
case m: ChangeUserPinStateReqMsg => logMessage(msg)
|
||||
case m: ChangeUserMobileFlagReqMsg => logMessage(msg)
|
||||
case m: UserConnectionAliveReqMsg => logMessage(msg)
|
||||
case m: UserConnectionUpdateRttReqMsg => logMessage(msg)
|
||||
case m: ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg => logMessage(msg)
|
||||
case m: ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsg => logMessage(msg)
|
||||
case m: ScreenshareRtmpBroadcastStartedEvtMsg => logMessage(msg)
|
||||
|
@ -51,6 +51,7 @@ postgres {
|
||||
}
|
||||
numThreads = 1
|
||||
maxConnections = 1
|
||||
queueSize = 20000
|
||||
}
|
||||
|
||||
|
||||
|
@ -293,6 +293,20 @@ object ChangeUserMobileFlagReqMsg { val NAME = "ChangeUserMobileFlagReqMsg" }
|
||||
case class ChangeUserMobileFlagReqMsg(header: BbbClientMsgHeader, body: ChangeUserMobileFlagReqMsgBody) extends StandardMsg
|
||||
case class ChangeUserMobileFlagReqMsgBody(userId: String, mobile: Boolean)
|
||||
|
||||
/**
|
||||
* Sent from client to inform the connection is alive.
|
||||
*/
|
||||
object UserConnectionAliveReqMsg { val NAME = "UserConnectionAliveReqMsg" }
|
||||
case class UserConnectionAliveReqMsg(header: BbbClientMsgHeader, body: UserConnectionAliveReqMsgBody) extends StandardMsg
|
||||
case class UserConnectionAliveReqMsgBody(userId: String)
|
||||
|
||||
/**
|
||||
* Sent from client to inform the RTT (time it took to send the Alive and receive confirmation).
|
||||
*/
|
||||
object UserConnectionUpdateRttReqMsg { val NAME = "UserConnectionUpdateRttReqMsg" }
|
||||
case class UserConnectionUpdateRttReqMsg(header: BbbClientMsgHeader, body: UserConnectionUpdateRttReqMsgBody) extends StandardMsg
|
||||
case class UserConnectionUpdateRttReqMsgBody(userId: String, networkRttInMs: Double)
|
||||
|
||||
/**
|
||||
* Sent to all clients about a user mobile flag.
|
||||
*/
|
||||
|
22
bbb-graphql-actions/src/actions/userSetConnectionAlive.ts
Normal file
22
bbb-graphql-actions/src/actions/userSetConnectionAlive.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { RedisMessage } from '../types';
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
const eventName = `UserConnectionAliveReqMsg`;
|
||||
|
||||
const routing = {
|
||||
meetingId: sessionVariables['x-hasura-meetingid'] as String,
|
||||
userId: sessionVariables['x-hasura-userid'] as String
|
||||
};
|
||||
|
||||
const header = {
|
||||
name: eventName,
|
||||
meetingId: routing.meetingId,
|
||||
userId: routing.userId
|
||||
};
|
||||
|
||||
const body = {
|
||||
userId: routing.userId,
|
||||
};
|
||||
|
||||
return { eventName, routing, header, body };
|
||||
}
|
23
bbb-graphql-actions/src/actions/userSetConnectionRtt.ts
Normal file
23
bbb-graphql-actions/src/actions/userSetConnectionRtt.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { RedisMessage } from '../types';
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
const eventName = `UserConnectionUpdateRttReqMsg`;
|
||||
|
||||
const routing = {
|
||||
meetingId: sessionVariables['x-hasura-meetingid'] as String,
|
||||
userId: sessionVariables['x-hasura-userid'] as String
|
||||
};
|
||||
|
||||
const header = {
|
||||
name: eventName,
|
||||
meetingId: routing.meetingId,
|
||||
userId: routing.userId
|
||||
};
|
||||
|
||||
const body = {
|
||||
userId: routing.userId,
|
||||
networkRttInMs: input.networkRttInMs
|
||||
};
|
||||
|
||||
return { eventName, routing, header, body };
|
||||
}
|
@ -27,16 +27,10 @@ export default function UserConnectionStatus() {
|
||||
//where is not necessary once user can update only its own status
|
||||
//Hasura accepts "now()" as value to timestamp fields
|
||||
const [updateUserClientResponseAtToMeAsNow] = useMutation(gql`
|
||||
mutation UpdateConnectionClientResponse($networkRttInMs: numeric) {
|
||||
update_user_connectionStatus(
|
||||
where: {userClientResponseAt: {_is_null: true}}
|
||||
_set: {
|
||||
userClientResponseAt: "now()",
|
||||
networkRttInMs: $networkRttInMs
|
||||
}
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
mutation UpdateConnectionRtt($networkRttInMs: Float!) {
|
||||
userSetConnectionRtt(
|
||||
networkRttInMs: $networkRttInMs
|
||||
)
|
||||
}
|
||||
`);
|
||||
|
||||
@ -50,13 +44,8 @@ export default function UserConnectionStatus() {
|
||||
|
||||
|
||||
const [updateConnectionAliveAtToMeAsNow] = useMutation(gql`
|
||||
mutation UpdateConnectionAliveAt($userId: String, $connectionAliveAt: timestamp) {
|
||||
update_user_connectionStatus(
|
||||
where: {},
|
||||
_set: { connectionAliveAt: "now()" }
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
mutation UpdateConnectionAliveAt {
|
||||
userSetConnectionAlive
|
||||
}
|
||||
`);
|
||||
|
||||
|
@ -3,6 +3,7 @@ BBB_GRAPHQL_MIDDLEWARE_LISTEN_PORT=8378
|
||||
BBB_GRAPHQL_MIDDLEWARE_REDIS_ADDRESS=127.0.0.1:6379
|
||||
BBB_GRAPHQL_MIDDLEWARE_REDIS_PASSWORD=
|
||||
BBB_GRAPHQL_MIDDLEWARE_HASURA_WS=ws://127.0.0.1:8080/v1/graphql
|
||||
BBB_GRAPHQL_MIDDLEWARE_RATE_LIMIT_IN_MS=50
|
||||
|
||||
# If you are running a cluster proxy setup, you need to configure the Origin of
|
||||
# the frontend. See https://docs.bigbluebutton.org/administration/cluster-proxy
|
||||
|
@ -1,14 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/common"
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/msgpatch"
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/websrv"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/time/rate"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -49,7 +52,26 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
http.HandleFunc("/", websrv.ConnectionHandler)
|
||||
//Define new Connections Rate Limit
|
||||
rateLimitInMs := 50
|
||||
if envRateLimitInMs := os.Getenv("BBB_GRAPHQL_MIDDLEWARE_RATE_LIMIT_IN_MS"); envRateLimitInMs != "" {
|
||||
if envRateLimitInMsAsInt, err := strconv.Atoi(envRateLimitInMs); err == nil {
|
||||
rateLimitInMs = envRateLimitInMsAsInt
|
||||
}
|
||||
}
|
||||
limiterInterval := rate.NewLimiter(rate.Every(time.Duration(rateLimitInMs)*time.Millisecond), 1)
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := limiterInterval.Wait(ctx); err != nil {
|
||||
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
websrv.ConnectionHandler(w, r)
|
||||
})
|
||||
|
||||
log.Infof("listening on %v:%v", listenIp, listenPort)
|
||||
log.Fatal(http.ListenAndServe(fmt.Sprintf("%v:%v", listenIp, listenPort), nil))
|
||||
|
@ -16,4 +16,5 @@ require (
|
||||
github.com/evanphx/json-patch v0.5.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
)
|
||||
|
@ -27,6 +27,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
@ -37,6 +37,13 @@ func (s *SafeChannel) ReceiveChannel() <-chan interface{} {
|
||||
return s.ch
|
||||
}
|
||||
|
||||
func (s *SafeChannel) Closed() bool {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
|
||||
return s.closed
|
||||
}
|
||||
|
||||
func (s *SafeChannel) Close() {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
@ -47,6 +54,10 @@ func (s *SafeChannel) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SafeChannel) Frozen() bool {
|
||||
return s.freezeFlag
|
||||
}
|
||||
|
||||
func (s *SafeChannel) FreezeChannel() {
|
||||
if !s.freezeFlag {
|
||||
s.mux.Lock()
|
||||
|
@ -43,10 +43,10 @@ type BrowserConnection struct {
|
||||
}
|
||||
|
||||
type HasuraConnection struct {
|
||||
Id string // hasura connection id
|
||||
Browserconn *BrowserConnection // browser connection that originated this hasura connection
|
||||
Websocket *websocket.Conn // websocket used to connect to hasura
|
||||
Context context.Context // hasura connection context (child of browser connection context)
|
||||
ContextCancelFunc context.CancelFunc // function to cancel the hasura context (and so, the hasura connection)
|
||||
MsgReceivingActiveChan *SafeChannel // indicate that it's waiting for the return of mutations before closing connection
|
||||
Id string // hasura connection id
|
||||
BrowserConn *BrowserConnection // browser connection that originated this hasura connection
|
||||
Websocket *websocket.Conn // websocket used to connect to hasura
|
||||
Context context.Context // hasura connection context (child of browser connection context)
|
||||
ContextCancelFunc context.CancelFunc // function to cancel the hasura context (and so, the hasura connection)
|
||||
FreezeMsgFromBrowserChan *SafeChannel // indicate that it's waiting for the return of mutations before closing connection
|
||||
}
|
||||
|
@ -60,11 +60,11 @@ func HasuraClient(browserConnection *common.BrowserConnection, cookies []*http.C
|
||||
defer hasuraConnectionContextCancel()
|
||||
|
||||
var thisConnection = common.HasuraConnection{
|
||||
Id: hasuraConnectionId,
|
||||
Browserconn: browserConnection,
|
||||
Context: hasuraConnectionContext,
|
||||
ContextCancelFunc: hasuraConnectionContextCancel,
|
||||
MsgReceivingActiveChan: common.NewSafeChannel(1),
|
||||
Id: hasuraConnectionId,
|
||||
BrowserConn: browserConnection,
|
||||
Context: hasuraConnectionContext,
|
||||
ContextCancelFunc: hasuraConnectionContextCancel,
|
||||
FreezeMsgFromBrowserChan: common.NewSafeChannel(1),
|
||||
}
|
||||
|
||||
browserConnection.HasuraConnection = &thisConnection
|
||||
|
@ -15,7 +15,7 @@ import (
|
||||
|
||||
// HasuraConnectionReader consumes messages from Hasura connection and add send to the browser channel
|
||||
func HasuraConnectionReader(hc *common.HasuraConnection, fromHasuraToBrowserChannel *common.SafeChannel, fromBrowserToHasuraChannel *common.SafeChannel, wg *sync.WaitGroup) {
|
||||
log := log.WithField("_routine", "HasuraConnectionReader").WithField("browserConnectionId", hc.Browserconn.Id).WithField("hasuraConnectionId", hc.Id)
|
||||
log := log.WithField("_routine", "HasuraConnectionReader").WithField("browserConnectionId", hc.BrowserConn.Id).WithField("hasuraConnectionId", hc.Id)
|
||||
defer log.Debugf("finished")
|
||||
log.Debugf("starting")
|
||||
|
||||
@ -28,9 +28,9 @@ func HasuraConnectionReader(hc *common.HasuraConnection, fromHasuraToBrowserChan
|
||||
err := wsjson.Read(hc.Context, hc.Websocket, &message)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
log.Debugf("Closing ws connection as Context was cancelled!")
|
||||
log.Debugf("Closing Hasura ws connection as Context was cancelled!")
|
||||
} else {
|
||||
log.Errorf("Error reading message from Hasura: %v", err)
|
||||
log.Debugf("Error reading message from Hasura: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -50,9 +50,9 @@ func handleMessageReceivedFromHasura(hc *common.HasuraConnection, fromHasuraToBr
|
||||
|
||||
//Check if subscription is still active!
|
||||
if queryId != "" {
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.RLock()
|
||||
subscription, ok := hc.Browserconn.ActiveSubscriptions[queryId]
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.RUnlock()
|
||||
hc.BrowserConn.ActiveSubscriptionsMutex.RLock()
|
||||
subscription, ok := hc.BrowserConn.ActiveSubscriptions[queryId]
|
||||
hc.BrowserConn.ActiveSubscriptionsMutex.RUnlock()
|
||||
if !ok {
|
||||
log.Debugf("Subscription with Id %s doesn't exist anymore, skiping response.", queryId)
|
||||
return
|
||||
@ -104,13 +104,13 @@ func handleSubscriptionMessage(hc *common.HasuraConnection, messageMap map[strin
|
||||
|
||||
//Store LastReceivedData Checksum
|
||||
subscription.LastReceivedDataChecksum = dataChecksum
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.Lock()
|
||||
hc.Browserconn.ActiveSubscriptions[queryId] = subscription
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.Unlock()
|
||||
hc.BrowserConn.ActiveSubscriptionsMutex.Lock()
|
||||
hc.BrowserConn.ActiveSubscriptions[queryId] = subscription
|
||||
hc.BrowserConn.ActiveSubscriptionsMutex.Unlock()
|
||||
|
||||
//Apply msg patch when it supports it
|
||||
if subscription.JsonPatchSupported {
|
||||
msgpatch.PatchMessage(&messageMap, queryId, dataKey, dataAsJson, hc.Browserconn)
|
||||
msgpatch.PatchMessage(&messageMap, queryId, dataKey, dataAsJson, hc.BrowserConn)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -126,17 +126,19 @@ func handleStreamingMessage(hc *common.HasuraConnection, messageMap map[string]i
|
||||
if lastCursor != nil && subscription.StreamCursorCurrValue != lastCursor {
|
||||
subscription.StreamCursorCurrValue = lastCursor
|
||||
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.Lock()
|
||||
hc.Browserconn.ActiveSubscriptions[queryId] = subscription
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.Unlock()
|
||||
hc.BrowserConn.ActiveSubscriptionsMutex.Lock()
|
||||
hc.BrowserConn.ActiveSubscriptions[queryId] = subscription
|
||||
hc.BrowserConn.ActiveSubscriptionsMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func handleCompleteMessage(hc *common.HasuraConnection, queryId string) {
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.Lock()
|
||||
delete(hc.Browserconn.ActiveSubscriptions, queryId)
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.Unlock()
|
||||
log.Debugf("Subscription with Id %s finished by Hasura.", queryId)
|
||||
hc.BrowserConn.ActiveSubscriptionsMutex.Lock()
|
||||
queryType := hc.BrowserConn.ActiveSubscriptions[queryId].Type
|
||||
operationName := hc.BrowserConn.ActiveSubscriptions[queryId].OperationName
|
||||
delete(hc.BrowserConn.ActiveSubscriptions, queryId)
|
||||
hc.BrowserConn.ActiveSubscriptionsMutex.Unlock()
|
||||
log.Debugf("%s (%s) with Id %s finished by Hasura.", queryType, operationName, queryId)
|
||||
}
|
||||
|
||||
func handleConnectionAckMessage(hc *common.HasuraConnection, messageMap map[string]interface{}, fromHasuraToBrowserChannel *common.SafeChannel, fromBrowserToHasuraChannel *common.SafeChannel) {
|
||||
@ -145,9 +147,9 @@ func handleConnectionAckMessage(hc *common.HasuraConnection, messageMap map[stri
|
||||
fromBrowserToHasuraChannel.UnfreezeChannel()
|
||||
|
||||
//Avoid to send `connection_ack` to the browser when it's a reconnection
|
||||
if hc.Browserconn.ConnAckSentToBrowser == false {
|
||||
if hc.BrowserConn.ConnAckSentToBrowser == false {
|
||||
fromHasuraToBrowserChannel.Send(messageMap)
|
||||
hc.Browserconn.ConnAckSentToBrowser = true
|
||||
hc.BrowserConn.ConnAckSentToBrowser = true
|
||||
}
|
||||
|
||||
go retransmiter.RetransmitSubscriptionStartMessages(hc, fromBrowserToHasuraChannel)
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
func HasuraConnectionWriter(hc *common.HasuraConnection, fromBrowserToHasuraChannel *common.SafeChannel, wg *sync.WaitGroup, initMessage map[string]interface{}) {
|
||||
log := log.WithField("_routine", "HasuraConnectionWriter")
|
||||
|
||||
browserConnection := hc.Browserconn
|
||||
browserConnection := hc.BrowserConn
|
||||
|
||||
log = log.WithField("browserConnectionId", browserConnection.Id).WithField("hasuraConnectionId", hc.Id)
|
||||
|
||||
@ -38,10 +38,12 @@ RangeLoop:
|
||||
select {
|
||||
case <-hc.Context.Done():
|
||||
break RangeLoop
|
||||
case <-hc.MsgReceivingActiveChan.ReceiveChannel():
|
||||
log.Debugf("freezing channel fromBrowserToHasuraChannel")
|
||||
//Freeze channel once it's about to close Hasura connection
|
||||
fromBrowserToHasuraChannel.FreezeChannel()
|
||||
case <-hc.FreezeMsgFromBrowserChan.ReceiveChannel():
|
||||
if !fromBrowserToHasuraChannel.Frozen() {
|
||||
log.Debug("freezing channel fromBrowserToHasuraChannel")
|
||||
//Freeze channel once it's about to close Hasura connection
|
||||
fromBrowserToHasuraChannel.FreezeChannel()
|
||||
}
|
||||
case fromBrowserMessage := <-fromBrowserToHasuraChannel.ReceiveChannel():
|
||||
{
|
||||
if fromBrowserMessage == nil {
|
||||
|
@ -6,10 +6,10 @@ import (
|
||||
)
|
||||
|
||||
func RetransmitSubscriptionStartMessages(hc *common.HasuraConnection, fromBrowserToHasuraChannel *common.SafeChannel) {
|
||||
log := log.WithField("_routine", "RetransmitSubscriptionStartMessages").WithField("browserConnectionId", hc.Browserconn.Id).WithField("hasuraConnectionId", hc.Id)
|
||||
log := log.WithField("_routine", "RetransmitSubscriptionStartMessages").WithField("browserConnectionId", hc.BrowserConn.Id).WithField("hasuraConnectionId", hc.Id)
|
||||
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.RLock()
|
||||
for _, subscription := range hc.Browserconn.ActiveSubscriptions {
|
||||
hc.BrowserConn.ActiveSubscriptionsMutex.RLock()
|
||||
for _, subscription := range hc.BrowserConn.ActiveSubscriptions {
|
||||
|
||||
//Not retransmitting Mutations
|
||||
if subscription.Type == common.Mutation {
|
||||
@ -27,5 +27,5 @@ func RetransmitSubscriptionStartMessages(hc *common.HasuraConnection, fromBrowse
|
||||
}
|
||||
}
|
||||
}
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.RUnlock()
|
||||
hc.BrowserConn.ActiveSubscriptionsMutex.RUnlock()
|
||||
}
|
||||
|
@ -47,11 +47,11 @@ func ConnectionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
acceptOptions.OriginPatterns = append(acceptOptions.OriginPatterns, bbbOrigin)
|
||||
}
|
||||
|
||||
c, err := websocket.Accept(w, r, &acceptOptions)
|
||||
browserWsConn, err := websocket.Accept(w, r, &acceptOptions)
|
||||
if err != nil {
|
||||
log.Errorf("error: %v", err)
|
||||
}
|
||||
defer c.Close(websocket.StatusInternalError, "the sky is falling")
|
||||
defer browserWsConn.Close(websocket.StatusInternalError, "the sky is falling")
|
||||
|
||||
var thisConnection = common.BrowserConnection{
|
||||
Id: browserConnectionId,
|
||||
@ -116,14 +116,16 @@ func ConnectionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
wgReader.Add(1)
|
||||
|
||||
// Reads from browser connection, writes into fromBrowserToHasuraChannel and fromBrowserToHasuraConnectionEstablishingChannel
|
||||
go reader.BrowserConnectionReader(browserConnectionId, browserConnectionContext, c, fromBrowserToHasuraChannel, fromBrowserToHasuraConnectionEstablishingChannel, []*sync.WaitGroup{&wgAll, &wgReader})
|
||||
go reader.BrowserConnectionReader(browserConnectionId, browserConnectionContext, browserConnectionContextCancel, browserWsConn, fromBrowserToHasuraChannel, fromBrowserToHasuraConnectionEstablishingChannel, []*sync.WaitGroup{&wgAll, &wgReader})
|
||||
go func() {
|
||||
wgReader.Wait()
|
||||
log.Debug("BrowserConnectionReader finished, closing Write Channel")
|
||||
fromHasuraToBrowserChannel.Close()
|
||||
thisConnection.Disconnected = true
|
||||
}()
|
||||
|
||||
// Reads from fromHasuraToBrowserChannel, writes to browser connection
|
||||
go writer.BrowserConnectionWriter(browserConnectionId, browserConnectionContext, c, fromHasuraToBrowserChannel, &wgAll)
|
||||
go writer.BrowserConnectionWriter(browserConnectionId, browserConnectionContext, browserWsConn, fromHasuraToBrowserChannel, &wgAll)
|
||||
|
||||
go ConnectionInitHandler(browserConnectionId, browserConnectionContext, fromBrowserToHasuraConnectionEstablishingChannel, &wgAll)
|
||||
|
||||
@ -136,8 +138,8 @@ func InvalidateSessionTokenConnections(sessionTokenToInvalidate string) {
|
||||
for _, browserConnection := range BrowserConnections {
|
||||
if browserConnection.SessionToken == sessionTokenToInvalidate {
|
||||
if browserConnection.HasuraConnection != nil {
|
||||
//Close chan to force stop receiving new messages from the browser
|
||||
browserConnection.HasuraConnection.MsgReceivingActiveChan.Close()
|
||||
//Send message to force stop receiving new messages from the browser
|
||||
browserConnection.HasuraConnection.FreezeMsgFromBrowserChan.Send(true)
|
||||
|
||||
// Wait until there are no active mutations
|
||||
for iterationCount := 0; iterationCount < 20; iterationCount++ {
|
||||
@ -157,9 +159,10 @@ func InvalidateSessionTokenConnections(sessionTokenToInvalidate string) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
log.Debugf("Processing invalidate request for sessionToken %v (hasura connection %v)", sessionTokenToInvalidate, browserConnection.HasuraConnection.Id)
|
||||
hasuraConnectionId := browserConnection.HasuraConnection.Id
|
||||
log.Debugf("Processing invalidate request for sessionToken %v (hasura connection %v)", sessionTokenToInvalidate, hasuraConnectionId)
|
||||
browserConnection.HasuraConnection.ContextCancelFunc()
|
||||
log.Debugf("Processed invalidate request for sessionToken %v (hasura connection %v)", sessionTokenToInvalidate, browserConnection.HasuraConnection.Id)
|
||||
log.Debugf("Processed invalidate request for sessionToken %v (hasura connection %v)", sessionTokenToInvalidate, hasuraConnectionId)
|
||||
|
||||
go SendUserGraphqlReconnectionForcedEvtMsg(browserConnection.SessionToken)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package reader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/common"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"nhooyr.io/websocket"
|
||||
@ -10,7 +11,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func BrowserConnectionReader(browserConnectionId string, ctx context.Context, c *websocket.Conn, fromBrowserToHasuraChannel *common.SafeChannel, fromBrowserToHasuraConnectionEstablishingChannel *common.SafeChannel, waitGroups []*sync.WaitGroup) {
|
||||
func BrowserConnectionReader(browserConnectionId string, ctx context.Context, ctxCancel context.CancelFunc, browserWsConn *websocket.Conn, fromBrowserToHasuraChannel *common.SafeChannel, fromBrowserToHasuraConnectionEstablishingChannel *common.SafeChannel, waitGroups []*sync.WaitGroup) {
|
||||
log := log.WithField("_routine", "BrowserConnectionReader").WithField("browserConnectionId", browserConnectionId)
|
||||
defer log.Debugf("finished")
|
||||
log.Debugf("starting")
|
||||
@ -29,14 +30,17 @@ func BrowserConnectionReader(browserConnectionId string, ctx context.Context, c
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
defer ctxCancel()
|
||||
|
||||
for {
|
||||
var v interface{}
|
||||
err := wsjson.Read(ctx, c, &v)
|
||||
err := wsjson.Read(ctx, browserWsConn, &v)
|
||||
if err != nil {
|
||||
log.Debugf("Browser is disconnected, skiping reading of ws message: %v", err)
|
||||
if errors.Is(err, context.Canceled) {
|
||||
log.Debugf("Closing Browser ws connection as Context was cancelled!")
|
||||
} else {
|
||||
log.Debugf("Hasura is disconnected, skiping reading of ws message: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,8 @@ func StartRedisListener() {
|
||||
messageCoreAsMap := messageAsMap["core"].(map[string]interface{})
|
||||
messageBodyAsMap := messageCoreAsMap["body"].(map[string]interface{})
|
||||
sessionTokenToInvalidate := messageBodyAsMap["sessionToken"]
|
||||
log.Debugf("Received invalidate request for sessionToken %v", sessionTokenToInvalidate)
|
||||
reason := messageBodyAsMap["reason"]
|
||||
log.Debugf("Received invalidate request for sessionToken %v (%v)", sessionTokenToInvalidate, reason)
|
||||
|
||||
//Not being used yet
|
||||
go InvalidateSessionTokenConnections(sessionTokenToInvalidate.(string))
|
||||
|
@ -4,13 +4,12 @@ import (
|
||||
"context"
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/common"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"sync"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"nhooyr.io/websocket/wsjson"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func BrowserConnectionWriter(browserConnectionId string, ctx context.Context, c *websocket.Conn, fromHasuraToBrowserChannel *common.SafeChannel, wg *sync.WaitGroup) {
|
||||
func BrowserConnectionWriter(browserConnectionId string, ctx context.Context, browserWsConn *websocket.Conn, fromHasuraToBrowserChannel *common.SafeChannel, wg *sync.WaitGroup) {
|
||||
log := log.WithField("_routine", "BrowserConnectionWriter").WithField("browserConnectionId", browserConnectionId)
|
||||
defer log.Debugf("finished")
|
||||
log.Debugf("starting")
|
||||
@ -20,13 +19,21 @@ RangeLoop:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Debug("Browser context cancelled.")
|
||||
break RangeLoop
|
||||
case toBrowserMessage := <-fromHasuraToBrowserChannel.ReceiveChannel():
|
||||
{
|
||||
if toBrowserMessage == nil {
|
||||
if fromHasuraToBrowserChannel.Closed() {
|
||||
break RangeLoop
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var toBrowserMessageAsMap = toBrowserMessage.(map[string]interface{})
|
||||
|
||||
log.Tracef("sending to browser: %v", toBrowserMessage)
|
||||
err := wsjson.Write(ctx, c, toBrowserMessage)
|
||||
err := wsjson.Write(ctx, browserWsConn, toBrowserMessage)
|
||||
if err != nil {
|
||||
log.Debugf("Browser is disconnected, skiping writing of ws message: %v", err)
|
||||
return
|
||||
@ -36,7 +43,7 @@ RangeLoop:
|
||||
// Authentication hook unauthorized this request
|
||||
if toBrowserMessageAsMap["type"] == "connection_error" {
|
||||
var payloadAsString = toBrowserMessageAsMap["payload"].(string)
|
||||
c.Close(websocket.StatusInternalError, payloadAsString)
|
||||
browserWsConn.Close(websocket.StatusInternalError, payloadAsString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,7 @@
|
||||
#!/bin/bash
|
||||
nodemon --exec go run cmd/bbb-graphql-middleware/main.go --signal SIGTERM
|
||||
|
||||
sudo systemctl stop bbb-graphql-middleware
|
||||
set -a # Automatically export all variables
|
||||
source ./bbb-graphql-middleware-config.env
|
||||
set +a # Stop automatically exporting
|
||||
go run cmd/bbb-graphql-middleware/main.go --signal SIGTERM
|
||||
|
@ -18,7 +18,6 @@ RestartSec=60
|
||||
SuccessExitStatus=143
|
||||
TimeoutStopSec=5
|
||||
PermissionsStartOnly=true
|
||||
LimitNOFILE=1024
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target bigbluebutton.target
|
||||
|
@ -451,6 +451,16 @@ type Mutation {
|
||||
): Boolean
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
userSetConnectionAlive: Boolean
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
userSetConnectionRtt(
|
||||
networkRttInMs: Float!
|
||||
): Boolean
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
userSetEmojiStatus(
|
||||
emoji: String!
|
||||
@ -544,4 +554,3 @@ input GuestUserApprovalStatus {
|
||||
guest: String!
|
||||
status: String!
|
||||
}
|
||||
|
||||
|
@ -409,6 +409,18 @@ actions:
|
||||
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
|
||||
permissions:
|
||||
- role: bbb_client
|
||||
- name: userSetConnectionAlive
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
|
||||
permissions:
|
||||
- role: bbb_client
|
||||
- name: userSetConnectionRtt
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
|
||||
permissions:
|
||||
- role: bbb_client
|
||||
- name: userSetEmojiStatus
|
||||
definition:
|
||||
kind: synchronous
|
||||
@ -486,5 +498,4 @@ custom_types:
|
||||
input_objects:
|
||||
- name: BreakoutRoom
|
||||
- name: GuestUserApprovalStatus
|
||||
objects: []
|
||||
scalars: []
|
||||
|
@ -184,6 +184,7 @@ select_permissions:
|
||||
- endedBy
|
||||
- endedByUserName
|
||||
- endedReasonCode
|
||||
- isBreakout
|
||||
- logoutUrl
|
||||
- meetingId
|
||||
- name
|
||||
|
@ -22,4 +22,4 @@ select_permissions:
|
||||
filter:
|
||||
meetingId:
|
||||
_eq: X-Hasura-MeetingId
|
||||
comment: ""
|
||||
comment: ""
|
@ -34,20 +34,3 @@ select_permissions:
|
||||
_eq: X-Hasura-MeetingId
|
||||
- userId:
|
||||
_eq: X-Hasura-UserId
|
||||
update_permissions:
|
||||
- role: bbb_client
|
||||
permission:
|
||||
columns:
|
||||
- connectionAliveAt
|
||||
- userClientResponseAt
|
||||
- networkRttInMs
|
||||
filter:
|
||||
_and:
|
||||
- meetingId:
|
||||
_eq: X-Hasura-MeetingId
|
||||
- userId:
|
||||
_eq: X-Hasura-UserId
|
||||
check: null
|
||||
set:
|
||||
meetingId: x-hasura-MeetingId
|
||||
userId: x-hasura-UserId
|
||||
|
@ -212,4 +212,4 @@ update_permissions:
|
||||
_eq: X-Hasura-MeetingId
|
||||
- userId:
|
||||
_eq: X-Hasura-UserId
|
||||
check: null
|
||||
check: null
|
@ -1 +1 @@
|
||||
git clone --branch v3.0.0-beta.4 --depth 1 https://github.com/bigbluebutton/bbb-webhooks bbb-webhooks
|
||||
git clone --branch v3.1.0 --depth 1 https://github.com/bigbluebutton/bbb-webhooks bbb-webhooks
|
||||
|
@ -1 +1 @@
|
||||
git clone --branch v0.6.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-recorder bbb-webrtc-recorder
|
||||
git clone --branch v0.7.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-recorder bbb-webrtc-recorder
|
||||
|
@ -1 +1 @@
|
||||
git clone --branch v2.13.0-alpha.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
git clone --branch v2.13.2 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
|
@ -1 +1 @@
|
||||
BIGBLUEBUTTON_RELEASE=3.0.0-alpha.4
|
||||
BIGBLUEBUTTON_RELEASE=3.0.0-alpha.5
|
||||
|
41
bigbluebutton-html5/client/clientStartup.tsx
Normal file
41
bigbluebutton-html5/client/clientStartup.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React, {
|
||||
Suspense,
|
||||
useContext,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
const MeetingClientLazy = React.lazy(() => import('./meetingClient'));
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
loadingClientLabel: {
|
||||
id: 'app.meeting.loadingClient',
|
||||
description: 'loading client label',
|
||||
},
|
||||
});
|
||||
|
||||
const ClientStartup: React.FC = () => {
|
||||
const loadingContextInfo = useContext(LoadingContext);
|
||||
const intl = useIntl();
|
||||
useEffect(() => {
|
||||
loadingContextInfo.setLoading(true, intl.formatMessage(intlMessages.loadingClientLabel));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
{
|
||||
(() => {
|
||||
try {
|
||||
return <MeetingClientLazy />;
|
||||
} catch (error) {
|
||||
loadingContextInfo.setLoading(false, '');
|
||||
throw new Error('Error on rendering MeetingClientLazy: '.concat(JSON.stringify(error) || ''));
|
||||
}
|
||||
})()
|
||||
}
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientStartup;
|
42
bigbluebutton-html5/client/main.tsx
Normal file
42
bigbluebutton-html5/client/main.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import ConnectionManager from '/imports/ui/components/connection-manager/component';
|
||||
// eslint-disable-next-line react/no-deprecated
|
||||
import { render } from 'react-dom';
|
||||
import SettingsLoader from '/imports/ui/components/settings-loader/component';
|
||||
import ErrorBoundary from '/imports/ui/components/common/error-boundary/component';
|
||||
import { ErrorScreen } from '/imports/ui/components/error-screen/component';
|
||||
import PresenceManager from '/imports/ui/components/join-handler/presenceManager/component';
|
||||
import LoadingScreenHOC from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
|
||||
import IntlLoaderContainer from '/imports/startup/client/intlLoader';
|
||||
import LocatedErrorBoundary from '/imports/ui/components/common/error-boundary/located-error-boundary/component';
|
||||
import StartupDataFetch from '/imports/ui/components/connection-manager/startup-data-fetch/component';
|
||||
import UserGrapQlMiniMongoAdapter from '/imports/ui/components/components-data/userGrapQlMiniMongoAdapter/component';
|
||||
import VoiceUserGrapQlMiniMongoAdapter from '/imports/ui/components/components-data/voiceUserGraphQlMiniMongoAdapter/component';
|
||||
|
||||
const Main: React.FC = () => {
|
||||
return (
|
||||
<StartupDataFetch>
|
||||
<ErrorBoundary Fallback={ErrorScreen}>
|
||||
<LoadingScreenHOC>
|
||||
<IntlLoaderContainer>
|
||||
{/* from there the error messages are located */}
|
||||
<LocatedErrorBoundary Fallback={ErrorScreen}>
|
||||
<ConnectionManager>
|
||||
<PresenceManager>
|
||||
<SettingsLoader />
|
||||
<UserGrapQlMiniMongoAdapter />
|
||||
<VoiceUserGrapQlMiniMongoAdapter />
|
||||
</PresenceManager>
|
||||
</ConnectionManager>
|
||||
</LocatedErrorBoundary>
|
||||
</IntlLoaderContainer>
|
||||
</LoadingScreenHOC>
|
||||
</ErrorBoundary>
|
||||
</StartupDataFetch>
|
||||
);
|
||||
};
|
||||
|
||||
render(
|
||||
<Main />,
|
||||
document.getElementById('app'),
|
||||
);
|
@ -17,43 +17,49 @@
|
||||
*/
|
||||
/* eslint no-unused-vars: 0 */
|
||||
|
||||
import React from 'react';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { render } from 'react-dom';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import '/imports/ui/services/mobile-app';
|
||||
import Base from '/imports/startup/client/base';
|
||||
import JoinHandler from '/imports/ui/components/join-handler/component';
|
||||
import JoinHandler from '../imports/ui/components/join-handler/component';
|
||||
import AuthenticatedHandler from '/imports/ui/components/authenticated-handler/component';
|
||||
import Subscriptions from '/imports/ui/components/subscriptions/component';
|
||||
import IntlStartup from '/imports/startup/client/intl';
|
||||
import ContextProviders from '/imports/ui/components/context-providers/component';
|
||||
import UsersAdapter from '/imports/ui/components/components-data/users-context/adapter';
|
||||
import GraphqlProvider from '/imports/ui/components/graphql-provider/component';
|
||||
import ConnectionManager from '/imports/ui/components/connection-manager/component';
|
||||
import { liveDataEventBrokerInitializer } from '/imports/ui/services/LiveDataEventBroker/LiveDataEventBroker';
|
||||
// The adapter import is "unused" as far as static code is concerned, but it
|
||||
// needs to here to override global prototypes. So: don't remove it - prlanzarin 25 Apr 2022
|
||||
import adapter from 'webrtc-adapter';
|
||||
|
||||
import collectionMirrorInitializer from './collection-mirror-initializer';
|
||||
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
|
||||
import IntlAdapter from '/imports/startup/client/intlAdapter';
|
||||
import PresenceAdapter from '/imports/ui/components/authenticated-handler/presence-adapter/component';
|
||||
import CustomUsersSettings from '/imports/ui/components/join-handler/custom-users-settings/component';
|
||||
|
||||
import('/imports/api/audio/client/bridge/bridge-whitelist').catch(() => {
|
||||
// bridge loading
|
||||
});
|
||||
|
||||
const { disableWebsocketFallback } = Meteor.settings.public.app;
|
||||
|
||||
if (disableWebsocketFallback) {
|
||||
Meteor.connection._stream._sockjsProtocolsWhitelist = function () { return ['websocket']; };
|
||||
|
||||
Meteor.disconnect();
|
||||
Meteor.reconnect();
|
||||
}
|
||||
|
||||
collectionMirrorInitializer();
|
||||
liveDataEventBrokerInitializer();
|
||||
|
||||
Meteor.startup(() => {
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
const Startup = () => {
|
||||
const loadingContextInfo = useContext(LoadingContext);
|
||||
useEffect(() => {
|
||||
const { disableWebsocketFallback } = window.meetingClientSettings.public.app;
|
||||
loadingContextInfo.setLoading(false, '');
|
||||
if (disableWebsocketFallback) {
|
||||
Meteor.connection._stream._sockjsProtocolsWhitelist = function () { return ['websocket']; };
|
||||
|
||||
Meteor.disconnect();
|
||||
Meteor.reconnect();
|
||||
}
|
||||
}, []);
|
||||
// Logs all uncaught exceptions to the client logger
|
||||
window.addEventListener('error', (e) => {
|
||||
let message = e.message || e.error.toString();
|
||||
@ -76,24 +82,20 @@ Meteor.startup(() => {
|
||||
}, message);
|
||||
});
|
||||
|
||||
// TODO make this a Promise
|
||||
render(
|
||||
return (
|
||||
<ContextProviders>
|
||||
<>
|
||||
<JoinHandler>
|
||||
<AuthenticatedHandler>
|
||||
<GraphqlProvider>
|
||||
<Subscriptions>
|
||||
<IntlStartup>
|
||||
<Base />
|
||||
</IntlStartup>
|
||||
</Subscriptions>
|
||||
</GraphqlProvider>
|
||||
</AuthenticatedHandler>
|
||||
</JoinHandler>
|
||||
<PresenceAdapter>
|
||||
<Subscriptions>
|
||||
<IntlAdapter>
|
||||
<Base />
|
||||
</IntlAdapter>
|
||||
</Subscriptions>
|
||||
</PresenceAdapter>
|
||||
<UsersAdapter />
|
||||
</>
|
||||
</ContextProviders>,
|
||||
document.getElementById('app'),
|
||||
</ContextProviders>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default Startup;
|
@ -2,8 +2,10 @@ import { Meteor } from 'meteor/meteor';
|
||||
import validateAuthToken from './methods/validateAuthToken';
|
||||
import setUserEffectiveConnectionType from './methods/setUserEffectiveConnectionType';
|
||||
import userActivitySign from './methods/userActivitySign';
|
||||
import validateConnection from './methods/validateConnection';
|
||||
|
||||
Meteor.methods({
|
||||
validateConnection,
|
||||
validateAuthToken,
|
||||
setUserEffectiveConnectionType,
|
||||
userActivitySign,
|
||||
|
@ -0,0 +1,65 @@
|
||||
import { Client } from 'pg';
|
||||
import upsertValidationState from '/imports/api/auth-token-validation/server/modifiers/upsertValidationState';
|
||||
import { ValidationStates } from '/imports/api/auth-token-validation';
|
||||
import userJoin from '/imports/api/users/server/handlers/userJoin';
|
||||
import Users from '/imports/api/users';
|
||||
|
||||
import createDummyUser from '../modifiers/createDummyUser';
|
||||
import updateUserConnectionId from '../modifiers/updateUserConnectionId';
|
||||
|
||||
async function validateConnection(requesterToken, meetingId, userId) {
|
||||
try {
|
||||
const client = new Client({
|
||||
host: process.env.POSTGRES_HOST || Meteor.settings.private.postgresql.host,
|
||||
port: process.env.POSTGRES_PORT || Meteor.settings.private.postgresql.port,
|
||||
database: process.env.POSTGRES_HOST || Meteor.settings.private.postgresql.database,
|
||||
user: process.env.POSTGRES_USER || Meteor.settings.private.postgresql.user,
|
||||
password: process.env.POSTGRES_PASSWORD || Meteor.settings.private.postgresql.password,
|
||||
query_timeout: process.env.POSTGRES_TIMEOUT || Meteor.settings.private.postgresql.timeout,
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
const res = await client.query('select "meetingId", "userId" from v_user_connection_auth where "authToken" = $1', [requesterToken]);
|
||||
|
||||
if (res.rows.length === 0) {
|
||||
await upsertValidationState(
|
||||
meetingId,
|
||||
userId,
|
||||
ValidationStates.INVALID,
|
||||
this.connection.id,
|
||||
);
|
||||
} else {
|
||||
const sessionId = `${meetingId}--${userId}`;
|
||||
this.setUserId(sessionId);
|
||||
await upsertValidationState(
|
||||
meetingId,
|
||||
userId,
|
||||
ValidationStates.VALIDATED,
|
||||
this.connection.id,
|
||||
);
|
||||
|
||||
const User = await Users.findOneAsync({
|
||||
meetingId,
|
||||
userId,
|
||||
});
|
||||
|
||||
if (!User) {
|
||||
await createDummyUser(meetingId, userId, requesterToken);
|
||||
} else {
|
||||
await updateUserConnectionId(meetingId, userId, this.connection.id);
|
||||
}
|
||||
userJoin(meetingId, userId, requesterToken);
|
||||
}
|
||||
await client.end();
|
||||
} catch (e) {
|
||||
await upsertValidationState(
|
||||
meetingId,
|
||||
userId,
|
||||
ValidationStates.INVALID,
|
||||
this.connection.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default validateConnection;
|
@ -25,7 +25,7 @@ import { useMutation } from '@apollo/client';
|
||||
import { SET_EXIT_REASON } from '/imports/ui/core/graphql/mutations/userMutations';
|
||||
import useUserChangedLocalSettings from '/imports/ui/services/settings/hooks/useUserChangedLocalSettings';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_group_id;
|
||||
const USER_WAS_EJECTED = 'userWasEjected';
|
||||
|
||||
@ -395,7 +395,7 @@ export default withTracker(() => {
|
||||
currentConnectionId: 1,
|
||||
connectionIdUpdateTime: 1,
|
||||
};
|
||||
const User = Users.findOne({ intId: credentials.requesterUserId }, { fields });
|
||||
const User = Users.findOne({ userId: credentials.requesterUserId }, { fields });
|
||||
const meeting = Meetings.findOne({ meetingId }, {
|
||||
fields: {
|
||||
meetingEnded: 1,
|
||||
@ -412,30 +412,21 @@ export default withTracker(() => {
|
||||
const ejected = User?.ejected;
|
||||
const ejectedReason = User?.ejectedReason;
|
||||
const meetingEndedReason = meeting?.meetingEndedReason;
|
||||
const currentConnectionId = User?.currentConnectionId;
|
||||
const { connectionID, connectionAuthTime } = Auth;
|
||||
const connectionIdUpdateTime = User?.connectionIdUpdateTime;
|
||||
|
||||
|
||||
if (ejected) {
|
||||
// use the connectionID to block users, so we can detect if the user was
|
||||
// blocked by the current connection. This is the case when a a user is
|
||||
// ejected from a meeting but not permanently ejected. Permanent ejects are
|
||||
// managed by the server, not by the client.
|
||||
BBBStorage.setItem(USER_WAS_EJECTED, connectionID);
|
||||
}
|
||||
|
||||
if (currentConnectionId && currentConnectionId !== connectionID && connectionIdUpdateTime > connectionAuthTime) {
|
||||
Session.set('codeError', '409');
|
||||
Session.set('errorMessageDescription', 'joined_another_window_reason')
|
||||
BBBStorage.setItem(USER_WAS_EJECTED, User.userId);
|
||||
}
|
||||
|
||||
let userSubscriptionHandler;
|
||||
|
||||
const codeError = Session.get('codeError');
|
||||
const { streams: usersVideo } = VideoService.getVideoStreams();
|
||||
|
||||
return {
|
||||
userWasEjected: (BBBStorage.getItem(USER_WAS_EJECTED) == connectionID),
|
||||
userWasEjected: (BBBStorage.getItem(USER_WAS_EJECTED)),
|
||||
approved,
|
||||
ejected,
|
||||
ejectedReason,
|
||||
|
@ -20,9 +20,9 @@ const propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
};
|
||||
|
||||
const DEFAULT_LANGUAGE = Meteor.settings.public.app.defaultSettings.application.fallbackLocale;
|
||||
const CLIENT_VERSION = Meteor.settings.public.app.html5ClientBuild;
|
||||
const FALLBACK_ON_EMPTY_STRING = Meteor.settings.public.app.fallbackOnEmptyLocaleString;
|
||||
const DEFAULT_LANGUAGE = window.meetingClientSettings.public.app.defaultSettings.application.fallbackLocale;
|
||||
const CLIENT_VERSION = window.meetingClientSettings.public.app.html5ClientBuild;
|
||||
const FALLBACK_ON_EMPTY_STRING = window.meetingClientSettings.public.app.fallbackOnEmptyLocaleString;
|
||||
|
||||
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
|
||||
const LARGE_FONT_LANGUAGES = ['te', 'km'];
|
||||
|
52
bigbluebutton-html5/imports/startup/client/intlAdapter.tsx
Normal file
52
bigbluebutton-html5/imports/startup/client/intlAdapter.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import { Session } from 'meteor/session';
|
||||
import { formatLocaleCode } from '/imports/utils/string-utils';
|
||||
import Intl from '/imports/ui/services/locale';
|
||||
import useCurrentLocale from '/imports/ui/core/local-states/useCurrentLocale';
|
||||
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
|
||||
|
||||
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
|
||||
const LARGE_FONT_LANGUAGES = ['te', 'km'];
|
||||
|
||||
interface IntlAdapterProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const IntlAdapter: React.FC<IntlAdapterProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [currentLocale] = useCurrentLocale();
|
||||
const intl = useIntl();
|
||||
const loadingContextInfo = useContext(LoadingContext);
|
||||
const setUp = () => {
|
||||
if (currentLocale) {
|
||||
const { language, formattedLocale } = formatLocaleCode(currentLocale);
|
||||
// @ts-ignore - JS code
|
||||
Settings.application.locale = currentLocale;
|
||||
Intl.setLocale(formattedLocale, intl.messages);
|
||||
if (RTL_LANGUAGES.includes(currentLocale.substring(0, 2))) {
|
||||
// @ts-ignore - JS code
|
||||
document.body.parentNode.setAttribute('dir', 'rtl');
|
||||
// @ts-ignore - JS code
|
||||
Settings.application.isRTL = true;
|
||||
} else {
|
||||
// @ts-ignore - JS code
|
||||
document.body.parentNode.setAttribute('dir', 'ltr');
|
||||
// @ts-ignore - JS code
|
||||
Settings.application.isRTL = false;
|
||||
}
|
||||
Session.set('isLargeFont', LARGE_FONT_LANGUAGES.includes(currentLocale.substring(0, 2)));
|
||||
document.getElementsByTagName('html')[0].lang = formattedLocale;
|
||||
document.body.classList.add(`lang-${language}`);
|
||||
Settings.save();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(setUp, []);
|
||||
useEffect(setUp, [currentLocale]);
|
||||
return !loadingContextInfo.isLoading ? children : null;
|
||||
};
|
||||
|
||||
export default IntlAdapter;
|
151
bigbluebutton-html5/imports/startup/client/intlLoader.tsx
Normal file
151
bigbluebutton-html5/imports/startup/client/intlLoader.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React, { useCallback, useContext, useEffect } from 'react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
|
||||
import useCurrentLocale from '/imports/ui/core/local-states/useCurrentLocale';
|
||||
import logger from './logger';
|
||||
|
||||
interface LocaleEndpointResponse {
|
||||
defaultLocale: string;
|
||||
fallbackOnEmptyLocaleString: boolean;
|
||||
normalizedLocale: string;
|
||||
regionDefaultLocale: string;
|
||||
}
|
||||
|
||||
interface LocaleJson {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface IntlLoaderContainerProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface IntlLoaderProps extends IntlLoaderContainerProps {
|
||||
currentLocale: string;
|
||||
setCurrentLocale: (locale: string) => void;
|
||||
}
|
||||
|
||||
const buildFetchLocale = (locale: string) => {
|
||||
const localesPath = 'locales';
|
||||
return new Promise((resolve) => {
|
||||
fetch(`${localesPath}/${locale}.json`)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return resolve(false);
|
||||
}
|
||||
return response.json()
|
||||
.then((jsonResponse) => resolve(jsonResponse))
|
||||
.catch(() => {
|
||||
logger.error({ logCode: 'intl_parse_locale_SyntaxError' }, `Could not parse locale file ${locale}.json, invalid json`);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const IntlLoader: React.FC<IntlLoaderProps> = ({
|
||||
children,
|
||||
currentLocale,
|
||||
setCurrentLocale,
|
||||
}) => {
|
||||
const loadingContextInfo = useContext(LoadingContext);
|
||||
|
||||
const [fetching, setFetching] = React.useState(false);
|
||||
const [normalizedLocale, setNormalizedLocale] = React.useState(navigator.language.replace('_', '-'));
|
||||
const [messages, setMessages] = React.useState<LocaleJson>({});
|
||||
const [fallbackOnEmptyLocaleString, setFallbackOnEmptyLocaleString] = React.useState(false);
|
||||
|
||||
const fetchLocalizedMessages = useCallback((locale: string, init: boolean) => {
|
||||
const url = `./locale?locale=${locale}&init=${init}`;
|
||||
setFetching(true);
|
||||
// fetch localized messages
|
||||
fetch(url)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
loadingContextInfo.setLoading(false, '');
|
||||
throw new Error('unable to fetch localized messages');
|
||||
}
|
||||
return response.json();
|
||||
}).then((data: LocaleEndpointResponse) => {
|
||||
const {
|
||||
defaultLocale,
|
||||
regionDefaultLocale,
|
||||
normalizedLocale,
|
||||
fallbackOnEmptyLocaleString: FOEL,
|
||||
} = data;
|
||||
setFallbackOnEmptyLocaleString(FOEL);
|
||||
const languageSets = Array.from(new Set([
|
||||
defaultLocale,
|
||||
regionDefaultLocale,
|
||||
normalizedLocale,
|
||||
])).filter((locale) => locale);
|
||||
|
||||
Promise.all(languageSets.map((locale) => buildFetchLocale(locale)))
|
||||
.then((resp) => {
|
||||
const typedResp = resp as Array<LocaleJson | boolean>;
|
||||
const foundLocales = typedResp.filter((locale) => locale instanceof Object) as LocaleJson[];
|
||||
if (foundLocales.length === 0) {
|
||||
const error = `${{ logCode: 'intl_fetch_locale_error' }},Could not fetch any locale file for ${languageSets.join(', ')}`;
|
||||
loadingContextInfo.setLoading(false, '');
|
||||
logger.error(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
const mergedLocale = foundLocales
|
||||
.reduce((acc, locale: LocaleJson) => Object.assign(acc, locale), {});
|
||||
const replacedLocale = normalizedLocale.replace('_', '-');
|
||||
setFetching(false);
|
||||
setNormalizedLocale(replacedLocale);
|
||||
setCurrentLocale(replacedLocale);
|
||||
setMessages(mergedLocale);
|
||||
if (!init) {
|
||||
loadingContextInfo.setLoading(false, '');
|
||||
}
|
||||
}).catch((error) => {
|
||||
loadingContextInfo.setLoading(false, '');
|
||||
throw new Error(error);
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const language = navigator.languages ? navigator.languages[0] : navigator.language;
|
||||
fetchLocalizedMessages(language, true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentLocale !== normalizedLocale) {
|
||||
fetchLocalizedMessages(currentLocale, false);
|
||||
}
|
||||
}, [currentLocale]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetching) {
|
||||
loadingContextInfo.setLoading(true, 'Fetching locale');
|
||||
}
|
||||
}, [fetching]);
|
||||
|
||||
return !fetching || Object.keys(messages).length > 0 ? (
|
||||
<IntlProvider
|
||||
fallbackOnEmptyString={fallbackOnEmptyLocaleString}
|
||||
locale={normalizedLocale.replace('_', '-').replace('@', '-')}
|
||||
messages={messages}
|
||||
>
|
||||
{children}
|
||||
</IntlProvider>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const IntlLoaderContainer: React.FC<IntlLoaderContainerProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [currentLocale, setCurrentLocale] = useCurrentLocale();
|
||||
return (
|
||||
<IntlLoader
|
||||
currentLocale={currentLocale}
|
||||
setCurrentLocale={setCurrentLocale}
|
||||
>
|
||||
{children}
|
||||
</IntlLoader>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntlLoaderContainer;
|
@ -1,4 +1,3 @@
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { createLogger, stdSerializers } from 'browser-bunyan';
|
||||
import { ConsoleFormattedStream } from '@browser-bunyan/console-formatted-stream';
|
||||
@ -15,8 +14,43 @@ import { nameFromLevel } from '@browser-bunyan/levels';
|
||||
// "url": "","method": ""}
|
||||
// externalURL is the end-point that logs will be sent to
|
||||
// Call the logger by doing a function call with the level name, I.e, logger.warn('Hi on warn')
|
||||
const fallback = { console: { enabled: true, level: 'info' } };
|
||||
const LOG_CONFIG = (JSON.parse(sessionStorage.getItem('clientStartupSettings') || '{}')?.clientLog) || fallback;
|
||||
|
||||
const LOG_CONFIG = Meteor.settings.public.clientLog || { console: { enabled: true, level: 'info' } };
|
||||
export function createStreamForTarget(target, options) {
|
||||
const TARGET_EXTERNAL = 'external';
|
||||
const TARGET_CONSOLE = 'console';
|
||||
const TARGET_SERVER = 'server';
|
||||
|
||||
let Stream = ConsoleRawStream;
|
||||
switch (target) {
|
||||
case TARGET_EXTERNAL:
|
||||
Stream = ServerLoggerStream;
|
||||
break;
|
||||
case TARGET_CONSOLE:
|
||||
Stream = ConsoleFormattedStream;
|
||||
break;
|
||||
case TARGET_SERVER:
|
||||
Stream = MeteorStream;
|
||||
break;
|
||||
default:
|
||||
Stream = ConsoleFormattedStream;
|
||||
}
|
||||
|
||||
return new Stream(options);
|
||||
}
|
||||
|
||||
export function generateLoggerStreams(config) {
|
||||
let result = [];
|
||||
Object.keys(config).forEach((key) => {
|
||||
const logOption = config[key];
|
||||
if (logOption && logOption.enabled) {
|
||||
const { level, ...streamOptions } = logOption;
|
||||
result = result.concat({ level, stream: createStreamForTarget(key, streamOptions) });
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Custom stream that logs to an end-point
|
||||
class ServerLoggerStream extends ServerStream {
|
||||
@ -26,16 +60,56 @@ class ServerLoggerStream extends ServerStream {
|
||||
if (params.logTag) {
|
||||
this.logTagString = params.logTag;
|
||||
}
|
||||
this.auth = null;
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
const streams = generateLoggerStreams(config);
|
||||
const { addStream } = this;
|
||||
streams.forEach((stream) => {
|
||||
addStream(stream);
|
||||
});
|
||||
}
|
||||
|
||||
setAuth(auth) {
|
||||
this.auth = auth;
|
||||
}
|
||||
|
||||
getUserData() {
|
||||
let userInfo = {};
|
||||
if (this.auth) {
|
||||
userInfo = this.auth.fullInfo;
|
||||
} else {
|
||||
userInfo = {
|
||||
meetingId: sessionStorage.getItem('meetingId'),
|
||||
userId: sessionStorage.getItem('userId'),
|
||||
logoutUrl: sessionStorage.getItem('logoutUrl'),
|
||||
sessionToken: sessionStorage.getItem('sessionToken'),
|
||||
userName: sessionStorage.getItem('userName'),
|
||||
extId: sessionStorage.getItem('extId'),
|
||||
meetingName: sessionStorage.getItem('meetingName'),
|
||||
};
|
||||
}
|
||||
|
||||
if (userInfo.meetingId) {
|
||||
userInfo = {
|
||||
sessionToken: sessionStorage.getItem('sessionToken'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
fullInfo: userInfo,
|
||||
};
|
||||
}
|
||||
|
||||
write(rec) {
|
||||
const { fullInfo } = Auth;
|
||||
const { fullInfo } = this.getUserData();
|
||||
|
||||
this.rec = rec;
|
||||
if (fullInfo.meetingId != null) {
|
||||
this.rec.userInfo = fullInfo;
|
||||
}
|
||||
this.rec.clientBuild = Meteor.settings.public.app.html5ClientBuild;
|
||||
this.rec.clientBuild = window.meetingClientSettings.public.app.html5ClientBuild;
|
||||
this.rec.connectionId = Meteor.connection._lastSessionId;
|
||||
if (this.logTagString) {
|
||||
this.rec.logTag = this.logTagString;
|
||||
@ -47,7 +121,7 @@ class ServerLoggerStream extends ServerStream {
|
||||
// Custom stream to log to the meteor server
|
||||
class MeteorStream {
|
||||
write(rec) {
|
||||
const { fullInfo } = Auth;
|
||||
const { fullInfo } = this.getUserData();
|
||||
const clientURL = window.location.href;
|
||||
|
||||
this.rec = rec;
|
||||
@ -78,42 +152,6 @@ class MeteorStream {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function createStreamForTarget(target, options) {
|
||||
const TARGET_EXTERNAL = 'external';
|
||||
const TARGET_CONSOLE = 'console';
|
||||
const TARGET_SERVER = 'server';
|
||||
|
||||
let Stream = ConsoleRawStream;
|
||||
switch (target) {
|
||||
case TARGET_EXTERNAL:
|
||||
Stream = ServerLoggerStream;
|
||||
break;
|
||||
case TARGET_CONSOLE:
|
||||
Stream = ConsoleFormattedStream;
|
||||
break;
|
||||
case TARGET_SERVER:
|
||||
Stream = MeteorStream;
|
||||
break;
|
||||
default:
|
||||
Stream = ConsoleFormattedStream;
|
||||
}
|
||||
|
||||
return new Stream(options);
|
||||
}
|
||||
|
||||
function generateLoggerStreams(config) {
|
||||
let result = [];
|
||||
Object.keys(config).forEach((key) => {
|
||||
const logOption = config[key];
|
||||
if (logOption && logOption.enabled) {
|
||||
const { level, ...streamOptions } = logOption;
|
||||
result = result.concat({ level, stream: createStreamForTarget(key, streamOptions) });
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Creates the logger with the array of streams of the chosen targets
|
||||
const logger = createLogger({
|
||||
name: 'clientLogger',
|
||||
@ -122,5 +160,4 @@ const logger = createLogger({
|
||||
src: true,
|
||||
});
|
||||
|
||||
|
||||
export default logger;
|
||||
|
@ -13,6 +13,10 @@ import { PrometheusAgent, METRIC_NAMES } from './prom-metrics/index.js'
|
||||
|
||||
let guestWaitHtml = '';
|
||||
|
||||
const DEFAULT_LANGUAGE = Meteor.settings.public.app.defaultSettings.application.fallbackLocale;
|
||||
const CLIENT_VERSION = Meteor.settings.public.app.html5ClientBuild;
|
||||
const FALLBACK_ON_EMPTY_STRING = Meteor.settings.public.app.fallbackOnEmptyLocaleString;
|
||||
|
||||
const env = Meteor.isDevelopment ? 'development' : 'production';
|
||||
|
||||
const meteorRoot = fs.realpathSync(`${process.cwd()}/../`);
|
||||
@ -273,6 +277,8 @@ WebApp.connectHandlers.use('/locale', (req, res) => {
|
||||
res.end(JSON.stringify({
|
||||
normalizedLocale: localeFile,
|
||||
regionDefaultLocale: (regionDefault && regionDefault !== localeFile) ? regionDefault : '',
|
||||
defaultLocale: DEFAULT_LANGUAGE,
|
||||
fallbackOnEmptyLocaleString: FALLBACK_ON_EMPTY_STRING,
|
||||
}));
|
||||
});
|
||||
|
||||
|
847
bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts
Normal file
847
bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts
Normal file
@ -0,0 +1,847 @@
|
||||
export interface MeetingClientSettings {
|
||||
public: Public
|
||||
private: Private
|
||||
}
|
||||
|
||||
export interface Public {
|
||||
app: App
|
||||
externalVideoPlayer: ExternalVideoPlayer
|
||||
kurento: Kurento
|
||||
syncUsersWithConnectionManager: SyncUsersWithConnectionManager
|
||||
poll: Poll
|
||||
captions: Captions
|
||||
timer: Timer
|
||||
chat: Chat
|
||||
userReaction: UserReaction
|
||||
userStatus: UserStatus
|
||||
notes: Notes
|
||||
layout: Layout
|
||||
pads: Pads
|
||||
media: Media
|
||||
stats: Stats
|
||||
presentation: Presentation
|
||||
selectRandomUser: SelectRandomUser
|
||||
user: User
|
||||
whiteboard: Whiteboard
|
||||
clientLog: ClientLog
|
||||
virtualBackgrounds: VirtualBackgrounds
|
||||
}
|
||||
|
||||
export interface App {
|
||||
instanceId: string
|
||||
mobileFontSize: string
|
||||
desktopFontSize: string
|
||||
audioChatNotification: boolean
|
||||
autoJoin: boolean
|
||||
listenOnlyMode: boolean
|
||||
forceListenOnly: boolean
|
||||
skipCheck: boolean
|
||||
skipCheckOnJoin: boolean
|
||||
enableDynamicAudioDeviceSelection: boolean
|
||||
clientTitle: string
|
||||
appName: string
|
||||
bbbServerVersion: string
|
||||
displayBbbServerVersion: boolean
|
||||
copyright: string
|
||||
html5ClientBuild: string
|
||||
helpLink: string
|
||||
delayForUnmountOfSharedNote: number
|
||||
bbbTabletApp: BbbTabletApp
|
||||
lockOnJoin: boolean
|
||||
cdn: string
|
||||
basename: string
|
||||
bbbWebBase: string
|
||||
learningDashboardBase: string
|
||||
customStyleUrl: string | null
|
||||
darkTheme: DarkTheme
|
||||
askForFeedbackOnLogout: boolean
|
||||
askForConfirmationOnLeave: boolean
|
||||
wakeLock: WakeLock
|
||||
allowDefaultLogoutUrl: boolean
|
||||
allowUserLookup: boolean
|
||||
dynamicGuestPolicy: boolean
|
||||
enableGuestLobbyMessage: boolean
|
||||
guestPolicyExtraAllowOptions: boolean
|
||||
alwaysShowWaitingRoomUI: boolean
|
||||
enableLimitOfViewersInWebcam: boolean
|
||||
enableMultipleCameras: boolean
|
||||
enableCameraAsContent: boolean
|
||||
enableWebcamSelectorButton: boolean
|
||||
enableTalkingIndicator: boolean
|
||||
enableCameraBrightness: boolean
|
||||
mirrorOwnWebcam: boolean
|
||||
viewersInWebcam: number
|
||||
ipv4FallbackDomain: string
|
||||
allowLogout: boolean
|
||||
allowFullscreen: boolean
|
||||
preloadNextSlides: number
|
||||
warnAboutUnsavedContentOnMeetingEnd: boolean
|
||||
audioCaptions: AudioCaptions
|
||||
mutedAlert: MutedAlert
|
||||
remainingTimeThreshold: number
|
||||
remainingTimeAlertThresholdArray: number[]
|
||||
enableDebugWindow: boolean
|
||||
breakouts: Breakouts
|
||||
customHeartbeat: boolean
|
||||
showAllAvailableLocales: boolean
|
||||
showAudioFilters: boolean
|
||||
raiseHandActionButton: RaiseHandActionButton
|
||||
reactionsButton: ReactionsButton
|
||||
emojiRain: EmojiRain
|
||||
enableNetworkStats: boolean
|
||||
enableCopyNetworkStatsButton: boolean
|
||||
userSettingsStorage: string
|
||||
defaultSettings: DefaultSettings
|
||||
shortcuts: Shortcuts
|
||||
branding: Branding
|
||||
connectionTimeout: number
|
||||
showHelpButton: boolean
|
||||
effectiveConnection: string[]
|
||||
fallbackOnEmptyLocaleString: boolean
|
||||
disableWebsocketFallback: boolean
|
||||
}
|
||||
|
||||
export interface BbbTabletApp {
|
||||
enabled: boolean
|
||||
iosAppStoreUrl: string
|
||||
iosAppUrlScheme: string
|
||||
}
|
||||
|
||||
export interface DarkTheme {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface WakeLock {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface AudioCaptions {
|
||||
enabled: boolean
|
||||
mobile: boolean
|
||||
provider: string
|
||||
language: Language
|
||||
}
|
||||
|
||||
export interface Language {
|
||||
available: string[]
|
||||
forceLocale: boolean
|
||||
defaultSelectLocale: boolean
|
||||
locale: string
|
||||
}
|
||||
|
||||
export interface MutedAlert {
|
||||
enabled: boolean
|
||||
interval: number
|
||||
threshold: number
|
||||
duration: number
|
||||
}
|
||||
|
||||
export interface Breakouts {
|
||||
allowUserChooseRoomByDefault: boolean
|
||||
captureWhiteboardByDefault: boolean
|
||||
captureSharedNotesByDefault: boolean
|
||||
sendInvitationToAssignedModeratorsByDefault: boolean
|
||||
breakoutRoomLimit: number
|
||||
}
|
||||
|
||||
export interface RaiseHandActionButton {
|
||||
enabled: boolean
|
||||
centered: boolean
|
||||
}
|
||||
|
||||
export interface ReactionsButton {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface EmojiRain {
|
||||
enabled: boolean
|
||||
intervalEmojis: number
|
||||
numberOfEmojis: number
|
||||
emojiSize: number
|
||||
}
|
||||
|
||||
export interface DefaultSettings {
|
||||
application: Application
|
||||
audio: Audio
|
||||
dataSaving: DataSaving
|
||||
}
|
||||
|
||||
export interface Application {
|
||||
selectedLayout: string
|
||||
animations: boolean
|
||||
chatAudioAlerts: boolean
|
||||
chatPushAlerts: boolean
|
||||
userJoinAudioAlerts: boolean
|
||||
userJoinPushAlerts: boolean
|
||||
userLeaveAudioAlerts: boolean
|
||||
userLeavePushAlerts: boolean
|
||||
raiseHandAudioAlerts: boolean
|
||||
raiseHandPushAlerts: boolean
|
||||
guestWaitingAudioAlerts: boolean
|
||||
guestWaitingPushAlerts: boolean
|
||||
wakeLock: boolean
|
||||
paginationEnabled: boolean
|
||||
whiteboardToolbarAutoHide: boolean
|
||||
autoCloseReactionsBar: boolean
|
||||
darkTheme: boolean
|
||||
fallbackLocale: string
|
||||
overrideLocale: string | null
|
||||
}
|
||||
|
||||
export interface Audio {
|
||||
inputDeviceId: string
|
||||
outputDeviceId: string
|
||||
}
|
||||
|
||||
export interface DataSaving {
|
||||
viewParticipantsWebcams: boolean
|
||||
viewScreenshare: boolean
|
||||
}
|
||||
|
||||
export interface Shortcuts {
|
||||
openOptions: OpenOptions
|
||||
toggleUserList: ToggleUserList
|
||||
toggleMute: ToggleMute
|
||||
joinAudio: JoinAudio
|
||||
leaveAudio: LeaveAudio
|
||||
togglePublicChat: TogglePublicChat
|
||||
hidePrivateChat: HidePrivateChat
|
||||
closePrivateChat: ClosePrivateChat
|
||||
raiseHand: RaiseHand
|
||||
openActions: OpenActions
|
||||
openDebugWindow: OpenDebugWindow
|
||||
}
|
||||
|
||||
export interface OpenOptions {
|
||||
accesskey: string
|
||||
descId: string
|
||||
}
|
||||
|
||||
export interface ToggleUserList {
|
||||
accesskey: string
|
||||
descId: string
|
||||
}
|
||||
|
||||
export interface ToggleMute {
|
||||
accesskey: string
|
||||
descId: string
|
||||
}
|
||||
|
||||
export interface JoinAudio {
|
||||
accesskey: string
|
||||
descId: string
|
||||
}
|
||||
|
||||
export interface LeaveAudio {
|
||||
accesskey: string
|
||||
descId: string
|
||||
}
|
||||
|
||||
export interface TogglePublicChat {
|
||||
accesskey: string
|
||||
descId: string
|
||||
}
|
||||
|
||||
export interface HidePrivateChat {
|
||||
accesskey: string
|
||||
descId: string
|
||||
}
|
||||
|
||||
export interface ClosePrivateChat {
|
||||
accesskey: string
|
||||
descId: string
|
||||
}
|
||||
|
||||
export interface RaiseHand {
|
||||
accesskey: string
|
||||
descId: string
|
||||
}
|
||||
|
||||
export interface OpenActions {
|
||||
accesskey: string
|
||||
descId: string
|
||||
}
|
||||
|
||||
export interface OpenDebugWindow {
|
||||
accesskey: string
|
||||
descId: string
|
||||
}
|
||||
|
||||
export interface Branding {
|
||||
displayBrandingArea: boolean
|
||||
}
|
||||
|
||||
export interface ExternalVideoPlayer {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface Kurento {
|
||||
wsUrl: string
|
||||
cameraWsOptions: CameraWsOptions
|
||||
gUMTimeout: number
|
||||
signalCandidates: boolean
|
||||
traceLogs: boolean
|
||||
cameraTimeouts: CameraTimeouts
|
||||
screenshare: Screenshare
|
||||
cameraProfiles: CameraProfile[]
|
||||
enableScreensharing: boolean
|
||||
enableVideo: boolean
|
||||
enableVideoMenu: boolean
|
||||
enableVideoPin: boolean
|
||||
autoShareWebcam: boolean
|
||||
skipVideoPreview: boolean
|
||||
skipVideoPreviewOnFirstJoin: boolean
|
||||
cameraSortingModes: CameraSortingModes
|
||||
cameraQualityThresholds: CameraQualityThresholds
|
||||
pagination: Pagination
|
||||
paginationThresholds: PaginationThresholds
|
||||
}
|
||||
|
||||
export interface CameraWsOptions {
|
||||
wsConnectionTimeout: number
|
||||
maxRetries: number
|
||||
debug: boolean
|
||||
heartbeat: Heartbeat
|
||||
}
|
||||
|
||||
export interface Heartbeat {
|
||||
interval: number
|
||||
delay: number
|
||||
reconnectOnFailure: boolean
|
||||
}
|
||||
|
||||
export interface CameraTimeouts {
|
||||
baseTimeout: number
|
||||
maxTimeout: number
|
||||
}
|
||||
|
||||
export interface Screenshare {
|
||||
enableVolumeControl: boolean
|
||||
subscriberOffering: boolean
|
||||
bitrate: number
|
||||
mediaTimeouts: MediaTimeouts
|
||||
constraints: Constraints
|
||||
}
|
||||
|
||||
export interface MediaTimeouts {
|
||||
maxConnectionAttempts: number
|
||||
baseTimeout: number
|
||||
baseReconnectionTimeout: number
|
||||
maxTimeout: number
|
||||
timeoutIncreaseFactor: number
|
||||
}
|
||||
|
||||
export interface Constraints {
|
||||
video: Video
|
||||
audio: boolean
|
||||
}
|
||||
|
||||
export interface Video {
|
||||
frameRate: FrameRate
|
||||
width: Width
|
||||
height: Height
|
||||
}
|
||||
|
||||
export interface FrameRate {
|
||||
ideal: number
|
||||
max: number
|
||||
}
|
||||
|
||||
export interface Width {
|
||||
max: number
|
||||
}
|
||||
|
||||
export interface Height {
|
||||
max: number
|
||||
}
|
||||
|
||||
export interface CameraProfile {
|
||||
id: string
|
||||
name: string
|
||||
bitrate: number
|
||||
hidden?: boolean
|
||||
default?: boolean
|
||||
constraints?: Constraints2
|
||||
}
|
||||
|
||||
export interface Constraints2 {
|
||||
width: number
|
||||
height: number
|
||||
frameRate?: number
|
||||
}
|
||||
|
||||
export interface CameraSortingModes {
|
||||
defaultSorting: string
|
||||
paginationSorting: string
|
||||
}
|
||||
|
||||
export interface CameraQualityThresholds {
|
||||
enabled: boolean
|
||||
applyConstraints: boolean
|
||||
privilegedStreams: boolean
|
||||
debounceTime: number
|
||||
thresholds: Threshold[]
|
||||
}
|
||||
|
||||
export interface Threshold {
|
||||
threshold: number
|
||||
profile: string
|
||||
}
|
||||
|
||||
export interface Pagination {
|
||||
paginationToggleEnabled: boolean
|
||||
pageChangeDebounceTime: number
|
||||
desktopPageSizes: DesktopPageSizes
|
||||
mobilePageSizes: MobilePageSizes
|
||||
desktopGridSizes: DesktopGridSizes
|
||||
mobileGridSizes: MobileGridSizes
|
||||
}
|
||||
|
||||
export interface DesktopPageSizes {
|
||||
moderator: number
|
||||
viewer: number
|
||||
}
|
||||
|
||||
export interface MobilePageSizes {
|
||||
moderator: number
|
||||
viewer: number
|
||||
}
|
||||
|
||||
export interface DesktopGridSizes {
|
||||
moderator: number
|
||||
viewer: number
|
||||
}
|
||||
|
||||
export interface MobileGridSizes {
|
||||
moderator: number
|
||||
viewer: number
|
||||
}
|
||||
|
||||
export interface PaginationThresholds {
|
||||
enabled: boolean
|
||||
thresholds: Threshold2[]
|
||||
}
|
||||
|
||||
export interface Threshold2 {
|
||||
users: number
|
||||
desktopPageSizes: DesktopPageSizes2
|
||||
}
|
||||
|
||||
export interface DesktopPageSizes2 {
|
||||
moderator: number
|
||||
viewer: number
|
||||
}
|
||||
|
||||
export interface SyncUsersWithConnectionManager {
|
||||
enabled: boolean
|
||||
syncInterval: number
|
||||
}
|
||||
|
||||
export interface Poll {
|
||||
enabled: boolean
|
||||
allowCustomResponseInput: boolean
|
||||
maxCustom: number
|
||||
maxTypedAnswerLength: number
|
||||
chatMessage: boolean
|
||||
}
|
||||
|
||||
export interface Captions {
|
||||
enabled: boolean
|
||||
id: string
|
||||
dictation: boolean
|
||||
background: string
|
||||
font: Font
|
||||
lines: number
|
||||
time: number
|
||||
}
|
||||
|
||||
export interface Font {
|
||||
color: string
|
||||
family: string
|
||||
size: string
|
||||
}
|
||||
|
||||
export interface Timer {
|
||||
enabled: boolean
|
||||
alarm: boolean
|
||||
music: Music
|
||||
interval: Interval
|
||||
time: number
|
||||
tabIndicator: boolean
|
||||
}
|
||||
|
||||
export interface Music {
|
||||
enabled: boolean
|
||||
volume: number
|
||||
track1: string
|
||||
track2: string
|
||||
track3: string
|
||||
}
|
||||
|
||||
export interface Interval {
|
||||
clock: number
|
||||
offset: number
|
||||
}
|
||||
|
||||
export interface Chat {
|
||||
enabled: boolean
|
||||
itemsPerPage: number
|
||||
timeBetweenFetchs: number
|
||||
enableSaveAndCopyPublicChat: boolean
|
||||
bufferChatInsertsMs: number
|
||||
startClosed: boolean
|
||||
min_message_length: number
|
||||
max_message_length: number
|
||||
grouping_messages_window: number
|
||||
type_system: string
|
||||
type_public: string
|
||||
type_private: string
|
||||
system_userid: string
|
||||
system_username: string
|
||||
public_id: string
|
||||
public_group_id: string
|
||||
public_userid: string
|
||||
public_username: string
|
||||
storage_key: string
|
||||
system_messages_keys: SystemMessagesKeys
|
||||
typingIndicator: TypingIndicator
|
||||
moderatorChatEmphasized: boolean
|
||||
autoConvertEmoji: boolean
|
||||
emojiPicker: EmojiPicker
|
||||
disableEmojis: string[]
|
||||
allowedElements: string[]
|
||||
}
|
||||
|
||||
export interface SystemMessagesKeys {
|
||||
chat_clear: string
|
||||
chat_poll_result: string
|
||||
chat_exported_presentation: string
|
||||
chat_status_message: string
|
||||
}
|
||||
|
||||
export interface TypingIndicator {
|
||||
enabled: boolean
|
||||
showNames: boolean
|
||||
}
|
||||
|
||||
export interface EmojiPicker {
|
||||
enable: boolean
|
||||
}
|
||||
|
||||
export interface UserReaction {
|
||||
enabled: boolean
|
||||
expire: number
|
||||
reactions: Reaction[]
|
||||
}
|
||||
|
||||
export interface Reaction {
|
||||
id: string
|
||||
native: string
|
||||
}
|
||||
|
||||
export interface UserStatus {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface Notes {
|
||||
enabled: boolean
|
||||
id: string
|
||||
pinnable: boolean
|
||||
}
|
||||
|
||||
export interface Layout {
|
||||
hidePresentationOnJoin: boolean
|
||||
showParticipantsOnLogin: boolean
|
||||
showPushLayoutButton: boolean
|
||||
showPushLayoutToggle: boolean
|
||||
}
|
||||
|
||||
export interface Pads {
|
||||
url: string
|
||||
cookie: Cookie
|
||||
}
|
||||
|
||||
export interface Cookie {
|
||||
path: string
|
||||
sameSite: string
|
||||
secure: boolean
|
||||
}
|
||||
|
||||
export interface Media {
|
||||
audio: Audio2
|
||||
stunTurnServersFetchAddress: string
|
||||
cacheStunTurnServers: boolean
|
||||
fallbackStunServer: string
|
||||
forceRelay: boolean
|
||||
forceRelayOnFirefox: boolean
|
||||
mediaTag: string
|
||||
callTransferTimeout: number
|
||||
callHangupTimeout: number
|
||||
callHangupMaximumRetries: number
|
||||
echoTestNumber: string
|
||||
listenOnlyCallTimeout: number
|
||||
transparentListenOnly: boolean
|
||||
fullAudioOffering: boolean
|
||||
listenOnlyOffering: boolean
|
||||
iceGatheringTimeout: number
|
||||
audioConnectionTimeout: number
|
||||
audioReconnectionDelay: number
|
||||
audioReconnectionAttempts: number
|
||||
sipjsHackViaWs: boolean
|
||||
sipjsAllowMdns: boolean
|
||||
sip_ws_host: string
|
||||
toggleMuteThrottleTime: number
|
||||
websocketKeepAliveInterval: number
|
||||
websocketKeepAliveDebounce: number
|
||||
traceSip: boolean
|
||||
sdpSemantics: string
|
||||
localEchoTest: LocalEchoTest
|
||||
showVolumeMeter: boolean
|
||||
}
|
||||
|
||||
export interface Audio2 {
|
||||
defaultFullAudioBridge: string
|
||||
defaultListenOnlyBridge: string
|
||||
bridges: Bridge[]
|
||||
retryThroughRelay: boolean
|
||||
}
|
||||
|
||||
export interface Bridge {
|
||||
name: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export interface LocalEchoTest {
|
||||
enabled: boolean
|
||||
initialHearingState: boolean
|
||||
useRtcLoopbackInChromium: boolean
|
||||
delay: Delay
|
||||
}
|
||||
|
||||
export interface Delay {
|
||||
enabled: boolean
|
||||
delayTime: number
|
||||
maxDelayTime: number
|
||||
}
|
||||
|
||||
export interface Stats {
|
||||
enabled: boolean
|
||||
interval: number
|
||||
timeout: number
|
||||
log: boolean
|
||||
notification: Notification
|
||||
jitter: number[]
|
||||
loss: number[]
|
||||
rtt: number[]
|
||||
level: string[]
|
||||
help: string
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
warning: boolean
|
||||
error: boolean
|
||||
}
|
||||
|
||||
export interface Presentation {
|
||||
allowDownloadOriginal: boolean
|
||||
allowDownloadWithAnnotations: boolean
|
||||
allowSnapshotOfCurrentSlide: boolean
|
||||
panZoomThrottle: number
|
||||
restoreOnUpdate: boolean
|
||||
uploadEndpoint: string
|
||||
fileUploadConstraintsHint: boolean
|
||||
mirroredFromBBBCore: MirroredFromBbbcore
|
||||
uploadValidMimeTypes: UploadValidMimeType[]
|
||||
}
|
||||
|
||||
export interface MirroredFromBbbcore {
|
||||
uploadSizeMax: number
|
||||
uploadPagesMax: number
|
||||
}
|
||||
|
||||
export interface UploadValidMimeType {
|
||||
extension: string
|
||||
mime: string
|
||||
}
|
||||
|
||||
export interface SelectRandomUser {
|
||||
enabled: boolean
|
||||
countdown: boolean
|
||||
}
|
||||
|
||||
export interface User {
|
||||
role_moderator: string
|
||||
role_viewer: string
|
||||
label: Label
|
||||
}
|
||||
|
||||
export interface Label {
|
||||
moderator: boolean
|
||||
mobile: boolean
|
||||
guest: boolean
|
||||
sharingWebcam: boolean
|
||||
}
|
||||
|
||||
export interface Whiteboard {
|
||||
annotationsQueueProcessInterval: number
|
||||
cursorInterval: number
|
||||
pointerDiameter: number
|
||||
maxStickyNoteLength: number
|
||||
maxNumberOfAnnotations: number
|
||||
annotations: Annotations
|
||||
styles: Styles
|
||||
toolbar: Toolbar
|
||||
}
|
||||
|
||||
export interface Annotations {
|
||||
status: Status
|
||||
}
|
||||
|
||||
export interface Status {
|
||||
start: string
|
||||
update: string
|
||||
end: string
|
||||
}
|
||||
|
||||
export interface Styles {
|
||||
text: Text
|
||||
}
|
||||
|
||||
export interface Text {
|
||||
family: string
|
||||
}
|
||||
|
||||
export interface Toolbar {
|
||||
multiUserPenOnly: boolean
|
||||
colors: Color[]
|
||||
thickness: Thickness[]
|
||||
font_sizes: FontSize[]
|
||||
tools: Tool[]
|
||||
presenterTools: string[]
|
||||
multiUserTools: string[]
|
||||
}
|
||||
|
||||
export interface Color {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface Thickness {
|
||||
value: number
|
||||
}
|
||||
|
||||
export interface FontSize {
|
||||
value: number
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
icon: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface ClientLog {
|
||||
server: Server
|
||||
console: Console
|
||||
external: External
|
||||
}
|
||||
|
||||
export interface Server {
|
||||
enabled: boolean
|
||||
level: string
|
||||
}
|
||||
|
||||
export interface Console {
|
||||
enabled: boolean
|
||||
level: string
|
||||
}
|
||||
|
||||
export interface External {
|
||||
enabled: boolean
|
||||
level: string
|
||||
url: string
|
||||
method: string
|
||||
throttleInterval: number
|
||||
flushOnClose: boolean
|
||||
logTag: string
|
||||
}
|
||||
|
||||
export interface VirtualBackgrounds {
|
||||
enabled: boolean
|
||||
enableVirtualBackgroundUpload: boolean
|
||||
storedOnBBB: boolean
|
||||
showThumbnails: boolean
|
||||
imagesPath: string
|
||||
thumbnailsPath: string
|
||||
fileNames: string[]
|
||||
}
|
||||
|
||||
export interface Private {
|
||||
analytics: Analytics
|
||||
app: App2
|
||||
redis: Redis
|
||||
serverLog: ServerLog
|
||||
minBrowserVersions: MinBrowserVersion[]
|
||||
prometheus: Prometheus
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
includeChat: boolean
|
||||
}
|
||||
|
||||
export interface App2 {
|
||||
host: string
|
||||
localesUrl: string
|
||||
pencilChunkLength: number
|
||||
loadSlidesFromHttpAlways: boolean
|
||||
}
|
||||
|
||||
export interface Redis {
|
||||
host: string
|
||||
port: string
|
||||
timeout: number
|
||||
password: string | null
|
||||
debug: boolean
|
||||
metrics: Metrics
|
||||
channels: Channels
|
||||
subscribeTo: string[]
|
||||
async: string[]
|
||||
ignored: string[]
|
||||
}
|
||||
|
||||
export interface Metrics {
|
||||
queueMetrics: boolean
|
||||
metricsDumpIntervalMs: number
|
||||
metricsFolderPath: string
|
||||
removeMeetingOnEnd: boolean
|
||||
}
|
||||
|
||||
export interface Channels {
|
||||
toAkkaApps: string
|
||||
toThirdParty: string
|
||||
}
|
||||
|
||||
export interface ServerLog {
|
||||
level: string
|
||||
streamerLog: boolean
|
||||
includeServerInfo: boolean
|
||||
healthChecker: HealthChecker
|
||||
}
|
||||
|
||||
export interface HealthChecker {
|
||||
enable: boolean
|
||||
intervalMs: number
|
||||
}
|
||||
|
||||
export interface MinBrowserVersion {
|
||||
browser: string
|
||||
version: number | number[] | string
|
||||
}
|
||||
|
||||
export interface Prometheus {
|
||||
enabled: boolean
|
||||
path: string
|
||||
collectDefaultMetrics: boolean
|
||||
collectRedisMetrics: boolean
|
||||
}
|
||||
|
||||
export default MeetingClientSettings;
|
@ -1,7 +1,5 @@
|
||||
export interface Cameras {
|
||||
streamId: string;
|
||||
meetingId: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface PresPagesWritable {
|
||||
@ -42,16 +40,36 @@ export interface Voice {
|
||||
startTime: number;
|
||||
}
|
||||
|
||||
export interface CustomParameter {
|
||||
parameter: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Reaction {
|
||||
reactionEmoji: string;
|
||||
}
|
||||
|
||||
export interface UserClientSettings {
|
||||
userClientSettingsJson: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
authToken: string;
|
||||
userId: string;
|
||||
extId: string;
|
||||
name: string;
|
||||
nameSortable: string;
|
||||
banned: boolean;
|
||||
isModerator: boolean;
|
||||
clientType: string;
|
||||
disconnected: boolean;
|
||||
isOnline: boolean;
|
||||
isRunningEchoTest: boolean;
|
||||
echoTestRunningAt: number;
|
||||
ejectReason: string;
|
||||
ejectReasonCode: string;
|
||||
ejected: boolean;
|
||||
enforceLayout: boolean;
|
||||
role: string;
|
||||
color: string;
|
||||
avatar: string;
|
||||
@ -59,10 +77,19 @@ export interface User {
|
||||
presenter?: boolean;
|
||||
pinned?: boolean;
|
||||
guest?: boolean;
|
||||
guestStatus: string;
|
||||
joinErrorCode: string;
|
||||
joinErrorMessage: string;
|
||||
joined: boolean;
|
||||
loggedOut: boolean;
|
||||
mobile?: boolean;
|
||||
whiteboardAccess?: boolean;
|
||||
isDialIn: boolean;
|
||||
voice?: Partial<Voice>;
|
||||
locked: boolean;
|
||||
registeredAt: number;
|
||||
registeredOn: string;
|
||||
hasDrawPermissionOnCurrentPage: boolean;
|
||||
lastBreakoutRoom?: LastBreakoutRoom;
|
||||
cameras: Array<Cameras>;
|
||||
presPagesWritable: Array<PresPagesWritable>;
|
||||
@ -72,4 +99,6 @@ export interface User {
|
||||
away: boolean;
|
||||
raiseHand: boolean;
|
||||
reaction: Reaction;
|
||||
customParameters: Array<CustomParameter>;
|
||||
userClientSettings: UserClientSettings;
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ const AboutContainer = (props) => {
|
||||
|
||||
const getClientBuildInfo = () => (
|
||||
{
|
||||
settings: Meteor.settings.public.app,
|
||||
settings: window.meetingClientSettings.public.app,
|
||||
|
||||
}
|
||||
);
|
||||
|
@ -77,13 +77,13 @@ const ActionsBarContainer = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const SELECT_RANDOM_USER_ENABLED = Meteor.settings.public.selectRandomUser.enabled;
|
||||
const RAISE_HAND_BUTTON_ENABLED = Meteor.settings.public.app.raiseHandActionButton.enabled;
|
||||
const RAISE_HAND_BUTTON_CENTERED = Meteor.settings.public.app.raiseHandActionButton.centered;
|
||||
const SELECT_RANDOM_USER_ENABLED = window.meetingClientSettings.public.selectRandomUser.enabled;
|
||||
const RAISE_HAND_BUTTON_ENABLED = window.meetingClientSettings.public.app.raiseHandActionButton.enabled;
|
||||
const RAISE_HAND_BUTTON_CENTERED = window.meetingClientSettings.public.app.raiseHandActionButton.centered;
|
||||
|
||||
const isReactionsButtonEnabled = () => {
|
||||
const USER_REACTIONS_ENABLED = Meteor.settings.public.userReaction.enabled;
|
||||
const REACTIONS_BUTTON_ENABLED = Meteor.settings.public.app.reactionsButton.enabled;
|
||||
const USER_REACTIONS_ENABLED = window.meetingClientSettings.public.userReaction.enabled;
|
||||
const REACTIONS_BUTTON_ENABLED = window.meetingClientSettings.public.app.reactionsButton.enabled;
|
||||
|
||||
return USER_REACTIONS_ENABLED && REACTIONS_BUTTON_ENABLED;
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ import { PANELS, ACTIONS } from '../../layout/enums';
|
||||
import { uniqueId, safeMatch } from '/imports/utils/string-utils';
|
||||
import PollService from '/imports/ui/components/poll/service';
|
||||
|
||||
const POLL_SETTINGS = Meteor.settings.public.poll;
|
||||
const POLL_SETTINGS = window.meetingClientSettings.public.poll;
|
||||
const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom;
|
||||
const MAX_CHAR_LIMIT = POLL_SETTINGS.maxTypedAnswerLength;
|
||||
const CANCELED_POLL_DELAY = 250;
|
||||
|
@ -11,7 +11,7 @@ import { useMutation } from '@apollo/client';
|
||||
|
||||
import Styled from './styles';
|
||||
|
||||
const REACTIONS = Meteor.settings.public.userReaction.reactions;
|
||||
const REACTIONS = window.meetingClientSettings.public.userReaction.reactions;
|
||||
|
||||
const ReactionsButton = (props) => {
|
||||
const {
|
||||
|
@ -81,7 +81,7 @@ class ActivityCheck extends Component {
|
||||
}
|
||||
|
||||
playAudioAlert() {
|
||||
this.alert = new Audio(`${Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename + Meteor.settings.public.app.instanceId}/resources/sounds/notify.mp3`);
|
||||
this.alert = new Audio(`${window.meetingClientSettings.public.app.cdn + window.meetingClientSettings.public.app.basename + window.meetingClientSettings.public.app.instanceId}/resources/sounds/notify.mp3`);
|
||||
this.alert.addEventListener('ended', () => { this.alert.src = null; });
|
||||
this.alert.play();
|
||||
}
|
||||
|
@ -57,11 +57,11 @@ import FloatingWindowContainer from '/imports/ui/components/floating-window/cont
|
||||
import ChatAlertContainerGraphql from '../chat/chat-graphql/alert/component';
|
||||
|
||||
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
const APP_CONFIG = window.meetingClientSettings.public.app;
|
||||
const DESKTOP_FONT_SIZE = APP_CONFIG.desktopFontSize;
|
||||
const MOBILE_FONT_SIZE = APP_CONFIG.mobileFontSize;
|
||||
const LAYOUT_CONFIG = Meteor.settings.public.layout;
|
||||
const CONFIRMATION_ON_LEAVE = Meteor.settings.public.app.askForConfirmationOnLeave;
|
||||
const LAYOUT_CONFIG = window.meetingClientSettings.public.layout;
|
||||
const CONFIRMATION_ON_LEAVE = window.meetingClientSettings.public.app.askForConfirmationOnLeave;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
userListLabel: {
|
||||
|
@ -37,7 +37,7 @@ import App from './component';
|
||||
import useToggleVoice from '../audio/audio-graphql/hooks/useToggleVoice';
|
||||
import useUserChangedLocalSettings from '../../services/settings/hooks/useUserChangedLocalSettings';
|
||||
|
||||
const CUSTOM_STYLE_URL = Meteor.settings.public.app.customStyleUrl;
|
||||
const CUSTOM_STYLE_URL = window.meetingClientSettings.public.app.customStyleUrl;
|
||||
|
||||
const endMeeting = (code, ejectedReason) => {
|
||||
Session.set('codeError', code);
|
||||
@ -339,7 +339,7 @@ export default withTracker(() => {
|
||||
customStyleUrl = CUSTOM_STYLE_URL;
|
||||
}
|
||||
|
||||
const LAYOUT_CONFIG = Meteor.settings.public.layout;
|
||||
const LAYOUT_CONFIG = window.meetingClientSettings.public.layout;
|
||||
|
||||
return {
|
||||
captions: CaptionsService.isCaptionsActive() ? <CaptionsContainer /> : null,
|
||||
@ -376,7 +376,7 @@ export default withTracker(() => {
|
||||
isLargeFont: Session.get('isLargeFont'),
|
||||
presentationRestoreOnUpdate: getFromUserSettings(
|
||||
'bbb_force_restore_presentation_on_new_events',
|
||||
Meteor.settings.public.presentation.restoreOnUpdate,
|
||||
window.meetingClientSettings.public.presentation.restoreOnUpdate,
|
||||
),
|
||||
hidePresentationOnJoin: getFromUserSettings('bbb_hide_presentation_on_join', LAYOUT_CONFIG.hidePresentationOnJoin),
|
||||
hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false),
|
||||
|
@ -5,12 +5,13 @@ import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import ButtonEmoji from '/imports/ui/components/common/button/button-emoji/ButtonEmoji';
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import Styled from './styles';
|
||||
import { getSpeechVoices, setAudioCaptions, setSpeechLocale } from '../service';
|
||||
import {
|
||||
getSpeechVoices, isAudioTranscriptionEnabled, setAudioCaptions, setSpeechLocale,
|
||||
} from '../service';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { MenuSeparatorItemType, MenuOptionItemType } from '/imports/ui/components/common/menu/menuTypes';
|
||||
import useAudioCaptionEnable from '/imports/ui/core/local-states/useAudioCaptionEnable';
|
||||
import { User } from '/imports/ui/Types/user';
|
||||
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { SET_SPEECH_LOCALE } from '/imports/ui/core/graphql/mutations/userMutations';
|
||||
|
||||
@ -260,27 +261,15 @@ const AudioCaptionsButtonContainer: React.FC = () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const {
|
||||
data: currentMeeting,
|
||||
loading: componentsFlagsLoading,
|
||||
} = useMeeting((m) => {
|
||||
return {
|
||||
componentsFlags: m.componentsFlags,
|
||||
};
|
||||
});
|
||||
if (currentUserLoading || componentsFlagsLoading) return null;
|
||||
if (!currentUser || !currentMeeting) return null;
|
||||
if (currentUserLoading) return null;
|
||||
if (!currentUser) return null;
|
||||
|
||||
const availableVoices = getSpeechVoices();
|
||||
const currentSpeechLocale = currentUser.speechLocale || '';
|
||||
const isSupported = availableVoices.length > 0;
|
||||
const isVoiceUser = !!currentUser.voice;
|
||||
|
||||
const { componentsFlags } = currentMeeting;
|
||||
|
||||
const hasCaptions = componentsFlags?.hasCaption;
|
||||
|
||||
if (!hasCaptions) return null;
|
||||
if (!isAudioTranscriptionEnabled()) return null;
|
||||
|
||||
return (
|
||||
<AudioCaptionsButton
|
||||
|
@ -21,9 +21,9 @@ import ListenOnly from './buttons/listenOnly';
|
||||
import LiveSelection from './buttons/LiveSelection';
|
||||
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const { enableDynamicAudioDeviceSelection } = Meteor.settings.public.app;
|
||||
const { enableDynamicAudioDeviceSelection } = window.meetingClientSettings.public.app;
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const MUTE_ALERT_CONFIG = Meteor.settings.public.app.mutedAlert;
|
||||
const MUTE_ALERT_CONFIG = window.meetingClientSettings.public.app.mutedAlert;
|
||||
|
||||
// @ts-ignore - temporary while settings are still in .js
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
@ -7,9 +7,9 @@ import AudioManager from '/imports/ui/services/audio-manager';
|
||||
|
||||
const MUTED_KEY = 'muted';
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
const APP_CONFIG = window.meetingClientSettings.public.app;
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const TOGGLE_MUTE_THROTTLE_TIME = Meteor.settings.public.media.toggleMuteThrottleTime;
|
||||
const TOGGLE_MUTE_THROTTLE_TIME = window.meetingClientSettings.public.media.toggleMuteThrottleTime;
|
||||
const DEVICE_LABEL_MAX_LENGTH = 40;
|
||||
const CLIENT_DID_USER_SELECTED_MICROPHONE_KEY = 'clientUserSelectedMicrophone';
|
||||
const CLIENT_DID_USER_SELECTED_LISTEN_ONLY_KEY = 'clientUserSelectedListenOnly';
|
||||
|
@ -19,7 +19,7 @@ import Service from '../service';
|
||||
|
||||
const AudioModalContainer = (props) => <AudioModal {...props} />;
|
||||
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
const APP_CONFIG = window.meetingClientSettings.public.app;
|
||||
|
||||
const invalidDialNumbers = ['0', '613-555-1212', '613-555-1234', '0000'];
|
||||
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
|
||||
|
@ -8,7 +8,7 @@ const AudioTestContainer = (props) => <AudioTest {...props} />;
|
||||
export default withTracker(() => ({
|
||||
outputDeviceId: Service.outputDeviceId(),
|
||||
handlePlayAudioSample: (deviceId) => {
|
||||
const sound = new Audio(`${Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename + Meteor.settings.public.app.instanceId}/resources/sounds/audioSample.mp3`);
|
||||
const sound = new Audio(`${window.meetingClientSettings.public.app.cdn + window.meetingClientSettings.public.app.basename + window.meetingClientSettings.public.app.instanceId}/resources/sounds/audioSample.mp3`);
|
||||
sound.addEventListener('ended', () => { sound.src = null; });
|
||||
if (deviceId && sound.setSinkId) sound.setSinkId(deviceId);
|
||||
sound.play();
|
||||
|
@ -0,0 +1,104 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import UserContainer from './user/container';
|
||||
|
||||
const CAPTIONS_CONFIG = window.meetingClientSettings.public.captions;
|
||||
|
||||
class LiveCaptions extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { clear: true };
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { clear } = this.state;
|
||||
|
||||
if (clear) {
|
||||
const { transcript } = this.props;
|
||||
if (prevProps.transcript !== transcript) {
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({ clear: false });
|
||||
}
|
||||
} else {
|
||||
this.resetTimer();
|
||||
this.timer = setTimeout(() => this.setState({ clear: true }), CAPTIONS_CONFIG.time);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.resetTimer();
|
||||
}
|
||||
|
||||
resetTimer() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
transcript,
|
||||
transcriptId,
|
||||
} = this.props;
|
||||
|
||||
const { clear } = this.state;
|
||||
|
||||
const hasContent = transcript.length > 0 && !clear;
|
||||
|
||||
const wrapperStyles = {
|
||||
display: 'flex',
|
||||
};
|
||||
|
||||
const captionStyles = {
|
||||
whiteSpace: 'pre-line',
|
||||
wordWrap: 'break-word',
|
||||
fontFamily: 'Verdana, Arial, Helvetica, sans-serif',
|
||||
fontSize: '1.5rem',
|
||||
background: '#000000a0',
|
||||
color: 'white',
|
||||
padding: hasContent ? '.5rem' : undefined,
|
||||
};
|
||||
|
||||
const visuallyHidden = {
|
||||
position: 'absolute',
|
||||
overflow: 'hidden',
|
||||
clip: 'rect(0 0 0 0)',
|
||||
height: '1px',
|
||||
width: '1px',
|
||||
margin: '-1px',
|
||||
padding: '0',
|
||||
border: '0',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={wrapperStyles}>
|
||||
{clear ? null : (
|
||||
<UserContainer
|
||||
background="#000000a0"
|
||||
transcriptId={transcriptId}
|
||||
/>
|
||||
)}
|
||||
<div style={captionStyles}>
|
||||
{clear ? '' : transcript}
|
||||
</div>
|
||||
<div
|
||||
style={visuallyHidden}
|
||||
aria-atomic
|
||||
aria-live="polite"
|
||||
>
|
||||
{clear ? '' : transcript}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LiveCaptions.propTypes = {
|
||||
transcript: PropTypes.string.isRequired,
|
||||
transcriptId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default LiveCaptions;
|
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Users from '/imports/api/users';
|
||||
import User from './component';
|
||||
|
||||
const MODERATOR = window.meetingClientSettings.public.user.role_moderator;
|
||||
|
||||
const Container = (props) => <User {...props} />;
|
||||
|
||||
const getUser = (userId) => {
|
||||
const user = Users.findOne(
|
||||
{ userId },
|
||||
{
|
||||
fields: {
|
||||
avatar: 1,
|
||||
color: 1,
|
||||
role: 1,
|
||||
name: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (user) {
|
||||
return {
|
||||
avatar: user.avatar,
|
||||
color: user.color,
|
||||
moderator: user.role === MODERATOR,
|
||||
name: user.name,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
avatar: '',
|
||||
color: '',
|
||||
moderator: false,
|
||||
name: '',
|
||||
};
|
||||
};
|
||||
|
||||
export default withTracker(({ transcriptId }) => {
|
||||
const userId = transcriptId.split('-')[0];
|
||||
|
||||
return getUser(userId);
|
||||
})(Container);
|
@ -7,7 +7,7 @@ import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import { isLiveTranscriptionEnabled } from '/imports/ui/services/features';
|
||||
import { unique } from 'radash';
|
||||
|
||||
const CONFIG = Meteor.settings.public.app.audioCaptions;
|
||||
const CONFIG = window.meetingClientSettings.public.app.audioCaptions;
|
||||
const ENABLED = CONFIG.enabled;
|
||||
const PROVIDER = CONFIG.provider;
|
||||
const LANGUAGES = CONFIG.language.available;
|
||||
|
@ -25,8 +25,8 @@ import Settings from '/imports/ui/services/settings';
|
||||
import useToggleVoice from './audio-graphql/hooks/useToggleVoice';
|
||||
import { usePreviousValue } from '/imports/ui/components/utils/hooks';
|
||||
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
const KURENTO_CONFIG = Meteor.settings.public.kurento;
|
||||
const APP_CONFIG = window.meetingClientSettings.public.app;
|
||||
const KURENTO_CONFIG = window.meetingClientSettings.public.kurento;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
joinedAudio: {
|
||||
|
@ -1,13 +1,13 @@
|
||||
import LocalPCLoopback from '/imports/ui/services/webrtc-base/local-pc-loopback';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
|
||||
const MEDIA_TAG = Meteor.settings.public.media.mediaTag;
|
||||
const USE_RTC_LOOPBACK_CHR = Meteor.settings.public.media.localEchoTest.useRtcLoopbackInChromium;
|
||||
const MEDIA_TAG = window.meetingClientSettings.public.media.mediaTag;
|
||||
const USE_RTC_LOOPBACK_CHR = window.meetingClientSettings.public.media.localEchoTest.useRtcLoopbackInChromium;
|
||||
const {
|
||||
enabled: DELAY_ENABLED = true,
|
||||
delayTime = 0.5,
|
||||
maxDelayTime = 2,
|
||||
} = Meteor.settings.public.media.localEchoTest.delay;
|
||||
} = window.meetingClientSettings.public.media.localEchoTest.delay;
|
||||
|
||||
let audioContext = null;
|
||||
let sourceContext = null;
|
||||
|
@ -8,13 +8,13 @@ import VoiceUsers from '/imports/api/voice-users';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import Storage from '../../services/storage/session';
|
||||
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
const TOGGLE_MUTE_THROTTLE_TIME = Meteor.settings.public.media.toggleMuteThrottleTime;
|
||||
const SHOW_VOLUME_METER = Meteor.settings.public.media.showVolumeMeter;
|
||||
const ROLE_MODERATOR = window.meetingClientSettings.public.user.role_moderator;
|
||||
const TOGGLE_MUTE_THROTTLE_TIME = window.meetingClientSettings.public.media.toggleMuteThrottleTime;
|
||||
const SHOW_VOLUME_METER = window.meetingClientSettings.public.media.showVolumeMeter;
|
||||
const {
|
||||
enabled: LOCAL_ECHO_TEST_ENABLED,
|
||||
initialHearingState: LOCAL_ECHO_INIT_HEARING_STATE,
|
||||
} = Meteor.settings.public.media.localEchoTest;
|
||||
} = window.meetingClientSettings.public.media.localEchoTest;
|
||||
|
||||
const MUTED_KEY = 'muted';
|
||||
|
||||
@ -73,7 +73,7 @@ const init = (messages, intl, toggleVoice) => {
|
||||
|
||||
const muteMicrophone = (toggleVoice) => {
|
||||
const user = VoiceUsers.findOne({
|
||||
meetingId: Auth.meetingID, intId: Auth.userID,
|
||||
userId: Auth.userID,
|
||||
}, { fields: { muted: 1 } });
|
||||
|
||||
if (!user.muted) {
|
||||
@ -87,14 +87,14 @@ const muteMicrophone = (toggleVoice) => {
|
||||
};
|
||||
|
||||
const isVoiceUser = () => {
|
||||
const voiceUser = VoiceUsers.findOne({ intId: Auth.userID },
|
||||
const voiceUser = VoiceUsers.findOne({ userId: Auth.userID },
|
||||
{ fields: { joined: 1 } });
|
||||
return voiceUser ? voiceUser.joined : false;
|
||||
};
|
||||
|
||||
const toggleMuteMicrophone = throttle((toggleVoice) => {
|
||||
const user = VoiceUsers.findOne({
|
||||
meetingId: Auth.meetingID, intId: Auth.userID,
|
||||
userId: Auth.userID,
|
||||
}, { fields: { muted: 1 } });
|
||||
|
||||
Storage.setItem(MUTED_KEY, !user.muted);
|
||||
|
@ -38,12 +38,10 @@ class AuthenticatedHandler extends Component {
|
||||
if (Auth.loggedIn) {
|
||||
callback();
|
||||
}
|
||||
|
||||
AuthenticatedHandler.addReconnectObservable();
|
||||
|
||||
const setReason = (reason) => {
|
||||
const log = reason.error === 403 ? 'warn' : 'error';
|
||||
|
||||
logger[log]({
|
||||
logCode: 'authenticatedhandlercomponent_setreason',
|
||||
extraInfo: { reason },
|
||||
@ -70,6 +68,7 @@ class AuthenticatedHandler extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
if (Session.get('codeError')) {
|
||||
console.log('Session.get(codeError)', Session.get('codeError'));
|
||||
this.setState({ authenticated: true });
|
||||
}
|
||||
AuthenticatedHandler.authenticatedRouteHandler((value, error) => {
|
||||
|
@ -0,0 +1,44 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import useAuthData from '/imports/ui/core/local-states/useAuthData';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { Session } from 'meteor/session';
|
||||
|
||||
interface PresenceAdapterProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const PresenceAdapter: React.FC<PresenceAdapterProps> = ({ children }) => {
|
||||
const [authData] = useAuthData();
|
||||
const [authSetted, setAuthSetted] = React.useState(false);
|
||||
useEffect(() => {
|
||||
const {
|
||||
authToken,
|
||||
logoutUrl,
|
||||
meetingId,
|
||||
sessionToken,
|
||||
userId,
|
||||
userName,
|
||||
extId,
|
||||
meetingName,
|
||||
} = authData;
|
||||
Auth.clearCredentials();
|
||||
Auth.set(
|
||||
meetingId,
|
||||
userId,
|
||||
authToken,
|
||||
logoutUrl,
|
||||
sessionToken,
|
||||
userName,
|
||||
extId,
|
||||
meetingName,
|
||||
);
|
||||
Auth.loggedIn = true;
|
||||
Auth.connectionAuthTime = new Date().getTime();
|
||||
Session.set('userWillAuth', false);
|
||||
setAuthSetted(true);
|
||||
}, []);
|
||||
|
||||
return authSetted ? children : null;
|
||||
};
|
||||
|
||||
export default PresenceAdapter;
|
@ -2,7 +2,6 @@ import React, { useMemo } from 'react';
|
||||
import ModalFullscreen from '/imports/ui/components/common/modal/fullscreen/component';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { range } from 'ramda';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { uniqueId } from '/imports/utils/string-utils';
|
||||
import { isImportPresentationWithAnnotationsFromBreakoutRoomsEnabled, isImportSharedNotesFromBreakoutRoomsEnabled } from '/imports/ui/services/features';
|
||||
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
||||
@ -27,7 +26,7 @@ import {
|
||||
} from './room-managment-state/types';
|
||||
import { BREAKOUT_ROOM_CREATE, BREAKOUT_ROOM_MOVE_USER } from '../../mutations';
|
||||
|
||||
const BREAKOUT_LIM = Meteor.settings.public.app.breakouts.breakoutRoomLimit;
|
||||
const BREAKOUT_LIM = window.meetingClientSettings.public.app.breakouts.breakoutRoomLimit;
|
||||
const MIN_BREAKOUT_ROOMS = 2;
|
||||
const MAX_BREAKOUT_ROOMS = BREAKOUT_LIM > MIN_BREAKOUT_ROOMS ? BREAKOUT_LIM : MIN_BREAKOUT_ROOMS;
|
||||
const MIN_BREAKOUT_TIME = 5;
|
||||
@ -315,7 +314,7 @@ const CreateBreakoutRoom: React.FC<CreateBreakoutRoomProps> = ({
|
||||
moveUser({
|
||||
variables: {
|
||||
userId,
|
||||
fromBreakoutRoomId: fromRoomId,
|
||||
fromBreakoutRoomId: fromRoomId || '',
|
||||
toBreakoutRoomId: toRoomId,
|
||||
},
|
||||
});
|
||||
|
@ -4,7 +4,7 @@ import MessageForm from './component';
|
||||
import ChatService from '/imports/ui/components/chat/service';
|
||||
import { BREAKOUT_ROOM_SEND_MESSAGE_TO_ALL } from '../mutations';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||
const MessageFormContainer = (props) => {
|
||||
const [sendMessageToAllBreakouts] = useMutation(BREAKOUT_ROOM_SEND_MESSAGE_TO_ALL);
|
||||
|
||||
|
@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Service from '/imports/ui/components/captions/service';
|
||||
|
||||
const CAPTIONS_CONFIG = Meteor.settings.public.captions;
|
||||
const CAPTIONS_CONFIG = window.meetingClientSettings.public.captions;
|
||||
|
||||
class LiveCaptions extends PureComponent {
|
||||
constructor(props) {
|
||||
|
@ -7,7 +7,7 @@ import { Meteor } from 'meteor/meteor';
|
||||
import { Session } from 'meteor/session';
|
||||
import { isCaptionsEnabled } from '/imports/ui/services/features';
|
||||
|
||||
const CAPTIONS_CONFIG = Meteor.settings.public.captions;
|
||||
const CAPTIONS_CONFIG = window.meetingClientSettings.public.captions;
|
||||
const LINE_BREAK = '\n';
|
||||
|
||||
const getAvailableLocales = () => {
|
||||
|
@ -17,10 +17,11 @@ import { generateExportedMessages } from './services';
|
||||
import { getDateString } from '/imports/utils/string-utils';
|
||||
import { ChatCommands } from '/imports/ui/core/enums/chat';
|
||||
import { CHAT_PUBLIC_CLEAR_HISTORY } from './mutations';
|
||||
import useMeetingSettings from '/imports/ui/core/local-states/useMeetingSettings';
|
||||
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const ENABLE_SAVE_AND_COPY_PUBLIC_CHAT = CHAT_CONFIG.enableSaveAndCopyPublicChat;
|
||||
// const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||
// const ENABLE_SAVE_AND_COPY_PUBLIC_CHAT = CHAT_CONFIG.enableSaveAndCopyPublicChat;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
clear: {
|
||||
@ -54,6 +55,9 @@ const intlMessages = defineMessages({
|
||||
});
|
||||
|
||||
const ChatActions: React.FC = () => {
|
||||
const [MeetingSettings] = useMeetingSettings();
|
||||
const chatConfig = MeetingSettings.public.chat;
|
||||
const { enableSaveAndCopyPublicChat } = chatConfig;
|
||||
const intl = useIntl();
|
||||
const isRTL = layoutSelect((i: Layout) => i.isRTL);
|
||||
const uniqueIdsRef = useRef<string[]>([uid(1), uid(2), uid(3), uid(4)]);
|
||||
@ -116,7 +120,7 @@ const ChatActions: React.FC = () => {
|
||||
const dropdownActions = [
|
||||
{
|
||||
key: uniqueIdsRef.current[0],
|
||||
enable: ENABLE_SAVE_AND_COPY_PUBLIC_CHAT,
|
||||
enable: enableSaveAndCopyPublicChat,
|
||||
icon: 'download',
|
||||
dataTest: 'chatSave',
|
||||
label: intl.formatMessage(intlMessages.save),
|
||||
@ -127,7 +131,7 @@ const ChatActions: React.FC = () => {
|
||||
},
|
||||
{
|
||||
key: uniqueIdsRef.current[1],
|
||||
enable: ENABLE_SAVE_AND_COPY_PUBLIC_CHAT,
|
||||
enable: enableSaveAndCopyPublicChat,
|
||||
icon: 'copy',
|
||||
id: 'clipboardButton',
|
||||
dataTest: 'chatCopy',
|
||||
|
@ -38,7 +38,7 @@ import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
||||
import { throttle } from '/imports/utils/throttle';
|
||||
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||
|
||||
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
||||
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
|
||||
@ -114,9 +114,9 @@ const messages = defineMessages({
|
||||
});
|
||||
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const AUTO_CONVERT_EMOJI = Meteor.settings.public.chat.autoConvertEmoji;
|
||||
const AUTO_CONVERT_EMOJI = window.meetingClientSettings.public.chat.autoConvertEmoji;
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const ENABLE_EMOJI_PICKER = Meteor.settings.public.chat.emojiPicker.enable;
|
||||
const ENABLE_EMOJI_PICKER = window.meetingClientSettings.public.chat.emojiPicker.enable;
|
||||
const ENABLE_TYPING_INDICATOR = CHAT_CONFIG.typingIndicator.enabled;
|
||||
|
||||
const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
||||
|
@ -1,15 +1,32 @@
|
||||
export const textToMarkdown = (message: string) => {
|
||||
let parsedMessage = message || '';
|
||||
parsedMessage = parsedMessage.trim();
|
||||
const parsedMessage = message || '';
|
||||
|
||||
// replace url with markdown links
|
||||
const urlRegex = /(?<!\]\()https?:\/\/([\w-]+\.)+\w{1,6}([/?=&#.]?[\w-]+)*/gm;
|
||||
parsedMessage = parsedMessage.replace(urlRegex, '[$&]($&)');
|
||||
// regular expression to match urls
|
||||
const urlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
|
||||
|
||||
// replace new lines with markdown new lines
|
||||
parsedMessage = parsedMessage.replace(/\n\r?/g, ' \n');
|
||||
// regular expression to match URLs with IP addresses
|
||||
const ipUrlRegex = /\b(?:https?:\/\/)?(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::\d{1,5})?(?:\/\S*)?\b/g;
|
||||
|
||||
return parsedMessage;
|
||||
// regular expression to match Markdown links
|
||||
const mdRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||
|
||||
// regular expression to match new lines
|
||||
const newLineRegex = /\n\r?/g;
|
||||
|
||||
// append https:// to URLs that don't have it
|
||||
const appendHttps = (match: string, text: string, url: string) => {
|
||||
if (!/^https?:\/\//.test(url)) {
|
||||
return `[${text}](https://${url})`;
|
||||
}
|
||||
return match;
|
||||
};
|
||||
|
||||
return parsedMessage
|
||||
.trim()
|
||||
.replace(urlRegex, '[$&]($&)')
|
||||
.replace(ipUrlRegex, '[$&]($&)')
|
||||
.replace(mdRegex, appendHttps)
|
||||
.replace(newLineRegex, ' \n');
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -5,7 +5,6 @@ import React, {
|
||||
useState,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { makeVar, useMutation } from '@apollo/client';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import LAST_SEEN_MUTATION from './queries';
|
||||
@ -27,7 +26,7 @@ import { Layout } from '../../../layout/layoutTypes';
|
||||
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
||||
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
|
||||
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
|
||||
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
Bar, BarChart, ResponsiveContainer, XAxis, YAxis,
|
||||
} from 'recharts';
|
||||
import caseInsensitiveReducer from '/imports/utils/caseInsensitiveReducer';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import Styled from './styles';
|
||||
|
||||
interface ChatPollContentProps {
|
||||
@ -25,6 +26,29 @@ interface Answers {
|
||||
id: number;
|
||||
}
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
true: {
|
||||
id: 'app.poll.t',
|
||||
description: 'Poll true option value',
|
||||
},
|
||||
false: {
|
||||
id: 'app.poll.f',
|
||||
description: 'Poll false option value',
|
||||
},
|
||||
yes: {
|
||||
id: 'app.poll.y',
|
||||
description: 'Poll yes option value',
|
||||
},
|
||||
no: {
|
||||
id: 'app.poll.n',
|
||||
description: 'Poll no option value',
|
||||
},
|
||||
abstention: {
|
||||
id: 'app.poll.abstention',
|
||||
description: 'Poll Abstention option value',
|
||||
},
|
||||
});
|
||||
|
||||
function assertAsMetadata(metadata: unknown): asserts metadata is Metadata {
|
||||
if (typeof metadata !== 'object' || metadata === null) {
|
||||
throw new Error('metadata is not an object');
|
||||
@ -52,14 +76,23 @@ function assertAsMetadata(metadata: unknown): asserts metadata is Metadata {
|
||||
const ChatPollContent: React.FC<ChatPollContentProps> = ({
|
||||
metadata: string,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const pollData = JSON.parse(string) as unknown;
|
||||
assertAsMetadata(pollData);
|
||||
|
||||
const answers = pollData.answers.reduce(
|
||||
caseInsensitiveReducer, [],
|
||||
);
|
||||
const answers = pollData.answers.reduce(caseInsensitiveReducer, []);
|
||||
|
||||
const height = answers.length * 50;
|
||||
const translatedAnswers = answers.map((answer: Answers) => {
|
||||
const translationKey = intlMessages[answer.key.toLowerCase() as keyof typeof intlMessages];
|
||||
const pollAnswer = translationKey ? intl.formatMessage(translationKey) : answer.key;
|
||||
return {
|
||||
...answer,
|
||||
pollAnswer,
|
||||
};
|
||||
});
|
||||
|
||||
const height = translatedAnswers.length * 50;
|
||||
return (
|
||||
<div data-test="chatPollMessageText">
|
||||
<Styled.PollText>
|
||||
@ -67,11 +100,11 @@ const ChatPollContent: React.FC<ChatPollContentProps> = ({
|
||||
</Styled.PollText>
|
||||
<ResponsiveContainer width="90%" height={height}>
|
||||
<BarChart
|
||||
data={answers}
|
||||
data={translatedAnswers}
|
||||
layout="vertical"
|
||||
>
|
||||
<XAxis type="number" />
|
||||
<YAxis width={80} type="category" dataKey="key" />
|
||||
<YAxis width={80} type="category" dataKey="pollAnswer" />
|
||||
<Bar dataKey="numVotes" fill="#0C57A7" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
@ -4,7 +4,7 @@ import Styled from './styles';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
const APP_CONFIG = window.meetingClientSettings.public.app;
|
||||
|
||||
interface ChatMessagePresentationContentProps {
|
||||
metadata: string;
|
||||
|
@ -14,7 +14,7 @@ const ChatMessageTextContent: React.FC<ChatMessageTextContentProps> = ({
|
||||
}) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const { allowedElements } = Meteor.settings.public.chat;
|
||||
const { allowedElements } = window.meetingClientSettings.public.chat;
|
||||
|
||||
return (
|
||||
<Styled.ChatMessage systemMsg={systemMsg} emphasizedMessage={emphasizedMessage} data-test="messageContent">
|
||||
|
@ -14,9 +14,10 @@ import ChatMessage from './chat-message/component';
|
||||
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
||||
import { useCreateUseSubscription } from '/imports/ui/core/hooks/createUseSubscription';
|
||||
import { setLoadedMessageGathering } from '/imports/ui/core/hooks/useLoadedChatMessages';
|
||||
import { ChatLoading } from '../../component';
|
||||
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
|
||||
|
||||
interface ChatListPageContainerProps {
|
||||
@ -113,7 +114,9 @@ const ChatListPageContainer: React.FC<ChatListPageContainerProps> = ({
|
||||
setLoadedMessageGathering(page, []);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!chatMessageData) return null;
|
||||
if (chatMessageData.length > 0 && chatId !== chatMessageData[0].chatId) return <ChatLoading isRTL={document.dir === 'rtl'} />;
|
||||
if (chatMessageData.length > 0 && chatMessageData[chatMessageData.length - 1].user?.userId) {
|
||||
setLastSender(page, chatMessageData[chatMessageData.length - 1].user?.userId);
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ interface ChatPopupProps {
|
||||
const WELCOME_MSG_KEY = 'welcomeMsg';
|
||||
const WELCOME_MSG_FOR_MODERATORS_KEY = 'welcomeMsgForModerators';
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
|
||||
|
||||
const setWelcomeMsgsOnSession = (key: string, value: boolean) => {
|
||||
|
@ -24,7 +24,7 @@ const DEBUG_CONSOLE = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
|
||||
const TYPING_INDICATOR_ENABLED = CHAT_CONFIG.typingIndicator.enabled;
|
||||
|
||||
|
@ -31,7 +31,7 @@ const Chat: React.FC<ChatProps> = ({ isRTL }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ChatLoading: React.FC<ChatProps> = ({ isRTL }) => {
|
||||
export const ChatLoading: React.FC<ChatProps> = ({ isRTL }) => {
|
||||
const { isChrome } = browserInfo;
|
||||
return (
|
||||
<Styled.Chat isRTL={isRTL} isChrome={isChrome}>
|
||||
|
@ -8,7 +8,8 @@ import { meetingIsBreakout } from '/imports/ui/components/app/service';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import PollService from '/imports/ui/components/poll/service';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const APP = window.meetingClientSettings.public.app;
|
||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||
const GROUPING_MESSAGES_WINDOW = CHAT_CONFIG.grouping_messages_window;
|
||||
|
||||
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
|
||||
@ -19,7 +20,7 @@ const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
|
||||
const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.system_messages_keys.chat_clear;
|
||||
const CHAT_POLL_RESULTS_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_poll_result;
|
||||
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
const ROLE_MODERATOR = window.meetingClientSettings.public.user.role_moderator;
|
||||
|
||||
const ScrollCollection = new Mongo.Collection(null);
|
||||
|
||||
|
@ -1,16 +1,43 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import logger, { generateLoggerStreams } from '/imports/startup/client/logger';
|
||||
|
||||
const propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
Fallback: PropTypes.func.isRequired,
|
||||
Fallback: PropTypes.element,
|
||||
errorMessage: PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
Fallback: null,
|
||||
errorMessage: 'Something went wrong',
|
||||
};
|
||||
|
||||
class ErrorBoundary extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { error: false, errorInfo: null };
|
||||
this.state = { error: '', errorInfo: null };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const data = JSON.parse((sessionStorage.getItem('clientStartupSettings')) || {});
|
||||
const logConfig = data?.clientLog;
|
||||
if (logConfig) {
|
||||
generateLoggerStreams(logConfig).forEach((stream) => {
|
||||
logger.addStream(stream);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { code, error, errorInfo } = this.state;
|
||||
const log = code === '403' ? 'warn' : 'error';
|
||||
if (error || errorInfo) {
|
||||
logger[log]({
|
||||
logCode: 'Error_Boundary_wrapper',
|
||||
extraInfo: { error, errorInfo },
|
||||
}, 'generic error boundary logger');
|
||||
}
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
@ -18,26 +45,27 @@ class ErrorBoundary extends Component {
|
||||
error,
|
||||
errorInfo,
|
||||
});
|
||||
logger.error({
|
||||
logCode: 'Error_Boundary_wrapper',
|
||||
extraInfo: { error, errorInfo },
|
||||
}, 'generic error boundary logger');
|
||||
}
|
||||
|
||||
render() {
|
||||
const { error } = this.state;
|
||||
const { children, Fallback } = this.props;
|
||||
const { error, errorInfo } = this.state;
|
||||
const { children, Fallback, errorMessage } = this.props;
|
||||
|
||||
return (error ? (<Fallback {...this.state} />) : children);
|
||||
const fallbackElement = Fallback && error
|
||||
? <Fallback error={error || {}} errorInfo={errorInfo} /> : <div>{errorMessage}</div>;
|
||||
return (error
|
||||
? fallbackElement
|
||||
: children);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorBoundary.propTypes = propTypes;
|
||||
ErrorBoundary.defaultProps = defaultProps;
|
||||
|
||||
export default ErrorBoundary;
|
||||
|
||||
export const withErrorBoundary = (WrappedComponent, FallbackComponent) => (props) => (
|
||||
<ErrorBoundary Fallback={FallbackComponent}>
|
||||
<WrappedComponent {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
export default ErrorBoundary;
|
||||
|
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import ErrorBoundary from '../component';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
errorMessage: {
|
||||
id: 'app.presentationUploder.genericError',
|
||||
defaultMessage: 'Something went wrong',
|
||||
},
|
||||
});
|
||||
|
||||
const LocatedErrorBoundary = ({ children, ...props }) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<ErrorBoundary
|
||||
{...props}
|
||||
errorMessage={intl.formatMessage(intlMessages.errorMessage)}
|
||||
>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocatedErrorBoundary;
|
@ -1,14 +1,11 @@
|
||||
import React from 'react';
|
||||
import Styled from './styles';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
|
||||
const { animations } = Settings.application;
|
||||
|
||||
const LoadingScreen = ({ children }) => (
|
||||
<Styled.Background>
|
||||
<Styled.Spinner animations={animations}>
|
||||
<Styled.Bounce1 animations={animations} />
|
||||
<Styled.Bounce2 animations={animations} />
|
||||
<Styled.Spinner animations>
|
||||
<Styled.Bounce1 animations />
|
||||
<Styled.Bounce2 animations />
|
||||
<div />
|
||||
</Styled.Spinner>
|
||||
<Styled.Message>
|
||||
|
@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import LoadingScreen from '../component';
|
||||
|
||||
interface LoadingContent {
|
||||
isLoading: boolean;
|
||||
loadingMessage: string;
|
||||
}
|
||||
|
||||
interface LoadingContextContent extends LoadingContent {
|
||||
setLoading: (isLoading: boolean, loadingMessage: string) => void;
|
||||
}
|
||||
|
||||
export const LoadingContext = React.createContext<LoadingContextContent>({
|
||||
isLoading: false,
|
||||
loadingMessage: '',
|
||||
setLoading: () => { },
|
||||
});
|
||||
|
||||
interface LoadingScreenHOCProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const LoadingScreenHOC: React.FC<LoadingScreenHOCProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [loading, setLoading] = React.useState<LoadingContent>({
|
||||
isLoading: false,
|
||||
loadingMessage: '',
|
||||
});
|
||||
|
||||
return (
|
||||
<LoadingContext.Provider value={{
|
||||
loadingMessage: loading.loadingMessage,
|
||||
isLoading: loading.isLoading,
|
||||
setLoading: (isLoading: boolean, loadingMessage: string = '') => {
|
||||
setLoading({
|
||||
isLoading,
|
||||
loadingMessage,
|
||||
});
|
||||
},
|
||||
}}
|
||||
>
|
||||
{
|
||||
loading.isLoading
|
||||
? (
|
||||
<LoadingScreen>
|
||||
<h1>{loading.loadingMessage}</h1>
|
||||
</LoadingScreen>
|
||||
)
|
||||
: null
|
||||
}
|
||||
{children}
|
||||
</LoadingContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingScreenHOC;
|
@ -22,7 +22,7 @@ class LocalesDropdown extends PureComponent {
|
||||
filterLocaleVariations(value) {
|
||||
const { allLocales } = this.props;
|
||||
if (allLocales) {
|
||||
if (Meteor.settings.public.app.showAllAvailableLocales) {
|
||||
if (window.meetingClientSettings.public.app.showAllAvailableLocales) {
|
||||
return allLocales;
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ import ModalSimple from '/imports/ui/components/common/modal/simple/component';
|
||||
import AudioService from '/imports/ui/components/audio/service';
|
||||
import Styled from './styles';
|
||||
|
||||
const SELECT_RANDOM_USER_COUNTDOWN = Meteor.settings.public.selectRandomUser.countdown;
|
||||
const SELECT_RANDOM_USER_COUNTDOWN = window.meetingClientSettings.public.selectRandomUser.countdown;
|
||||
|
||||
const messages = defineMessages({
|
||||
noViewers: {
|
||||
@ -102,9 +102,9 @@ class RandomUserSelect extends Component {
|
||||
}
|
||||
|
||||
play() {
|
||||
AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
|
||||
+ Meteor.settings.public.app.basename
|
||||
+ Meteor.settings.public.app.instanceId}`
|
||||
AudioService.playAlertSound(`${window.meetingClientSettings.public.app.cdn
|
||||
+ window.meetingClientSettings.public.app.basename
|
||||
+ window.meetingClientSettings.public.app.instanceId}`
|
||||
+ '/resources/sounds/Poll.mp3');
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { PICK_RANDOM_VIEWER } from '/imports/ui/core/graphql/mutations/userMutations';
|
||||
|
||||
const SELECT_RANDOM_USER_ENABLED = Meteor.settings.public.selectRandomUser.enabled;
|
||||
const SELECT_RANDOM_USER_ENABLED = window.meetingClientSettings.public.selectRandomUser.enabled;
|
||||
|
||||
// A value that is used by component to remember
|
||||
// whether it should be open or closed after a render
|
||||
|
@ -0,0 +1,37 @@
|
||||
import { useSubscription } from '@apollo/client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import CURRENT_USER_SUBSCRIPTION from '/imports/ui/core/graphql/queries/currentUserSubscription';
|
||||
import { User } from '/imports/ui/Types/user';
|
||||
import Users from '/imports/api/users';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
|
||||
interface UserCurrentResponse {
|
||||
user_current: Array<User>;
|
||||
}
|
||||
|
||||
const UserGrapQlMiniMongoAdapter: React.FC = () => {
|
||||
const {
|
||||
error,
|
||||
data,
|
||||
} = useSubscription<UserCurrentResponse>(CURRENT_USER_SUBSCRIPTION);
|
||||
const [userDataSetted, setUserDataSetted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
logger.error('Error in UserGrapQlMiniMongoAdapter', error);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data.user_current) {
|
||||
const { userId } = data.user_current[0];
|
||||
Users.upsert({ userId }, data.user_current[0]);
|
||||
if (!userDataSetted) {
|
||||
setUserDataSetted(true);
|
||||
}
|
||||
}
|
||||
}, [data]);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default UserGrapQlMiniMongoAdapter;
|
@ -0,0 +1,41 @@
|
||||
import { useSubscription } from '@apollo/client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import VoiceUsers from '/imports/api/voice-users/';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import { UserVoiceStreamResponse, voiceUserStream } from './queries';
|
||||
|
||||
const VoiceUserGrapQlMiniMongoAdapter: React.FC = () => {
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
data,
|
||||
} = useSubscription<UserVoiceStreamResponse>(voiceUserStream);
|
||||
const [voiceUserDataSetted, setVoiceUserDataSetted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
logger.error('Error in VoiceUserGrapQlMiniMongoAdapter', error);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data.user_voice_mongodb_adapter_stream) {
|
||||
const usersVoice = data.user_voice_mongodb_adapter_stream;
|
||||
|
||||
usersVoice.forEach((userVoice) => {
|
||||
VoiceUsers.upsert({ userId: userVoice.userId }, userVoice);
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
if (!voiceUserDataSetted) {
|
||||
setVoiceUserDataSetted(true);
|
||||
}
|
||||
}
|
||||
}, [loading]);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default VoiceUserGrapQlMiniMongoAdapter;
|
@ -0,0 +1,67 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
interface UserVoiceStream {
|
||||
callerName: string | null;
|
||||
callerNum: string | null;
|
||||
callingWith: string | null;
|
||||
endTime: string | null;
|
||||
endedAt: string | null;
|
||||
floor: string | null;
|
||||
hideTalkingIndicatorAt: string | null;
|
||||
joined: string | null;
|
||||
lastFloorTime: string | null;
|
||||
lastSpeakChangedAt: number;
|
||||
listenOnly: string | null;
|
||||
muted: string | null;
|
||||
showTalkingIndicator: boolean;
|
||||
spoke: string | null;
|
||||
startTime: string | null;
|
||||
startedAt: string | null;
|
||||
talking: string | null;
|
||||
userId: string;
|
||||
voiceConf: string | null;
|
||||
voiceConfCallSession: string | null;
|
||||
voiceConfCallState: string | null;
|
||||
voiceConfClientSession: string | null;
|
||||
voiceUpdatedAt: string | null;
|
||||
voiceUserId: string | null;
|
||||
}
|
||||
|
||||
export interface UserVoiceStreamResponse {
|
||||
user_voice_mongodb_adapter_stream: UserVoiceStream[];
|
||||
}
|
||||
|
||||
export const voiceUserStream = gql`
|
||||
subscription voiceUserStream {
|
||||
user_voice_mongodb_adapter_stream(cursor: {initial_value: {voiceUpdatedAt: "2020-01-01"}}, batch_size: 100) {
|
||||
callerName
|
||||
callerNum
|
||||
callingWith
|
||||
endTime
|
||||
endedAt
|
||||
floor
|
||||
hideTalkingIndicatorAt
|
||||
joined
|
||||
lastFloorTime
|
||||
lastSpeakChangedAt
|
||||
listenOnly
|
||||
muted
|
||||
showTalkingIndicator
|
||||
spoke
|
||||
startTime
|
||||
startedAt
|
||||
talking
|
||||
userId
|
||||
voiceConf
|
||||
voiceConfCallSession
|
||||
voiceConfCallState
|
||||
voiceConfClientSession
|
||||
voiceUpdatedAt
|
||||
voiceUserId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
voiceUserStream,
|
||||
};
|
@ -0,0 +1,107 @@
|
||||
import {
|
||||
ApolloClient, ApolloProvider, InMemoryCache, NormalizedCacheObject,
|
||||
} from '@apollo/client';
|
||||
import { WebSocketLink } from '@apollo/client/link/ws';
|
||||
import { SubscriptionClient } from 'subscriptions-transport-ws';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
|
||||
|
||||
interface ConnectionManagerProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface Response {
|
||||
response: {
|
||||
returncode: string;
|
||||
version: string;
|
||||
apiVersion: string;
|
||||
bbbVersion: string;
|
||||
graphqlWebsocketUrl: string;
|
||||
}
|
||||
}
|
||||
|
||||
const ConnectionManager: React.FC<ConnectionManagerProps> = ({ children }): React.ReactNode => {
|
||||
const [graphqlUrlApolloClient, setApolloClient] = React.useState<ApolloClient<NormalizedCacheObject> | null>(null);
|
||||
const [graphqlUrl, setGraphqlUrl] = React.useState<string>('');
|
||||
const loadingContextInfo = useContext(LoadingContext);
|
||||
useEffect(() => {
|
||||
fetch(`https://${window.location.hostname}/bigbluebutton/api`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(async (response) => {
|
||||
const responseJson: Response = await response.json();
|
||||
setGraphqlUrl(responseJson.response.graphqlWebsocketUrl);
|
||||
}).catch((error) => {
|
||||
loadingContextInfo.setLoading(false, '');
|
||||
throw new Error('Error fetching GraphQL URL: '.concat(error.message || ''));
|
||||
});
|
||||
loadingContextInfo.setLoading(true, 'Fetching GraphQL URL');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadingContextInfo.setLoading(true, 'Connecting to GraphQL server');
|
||||
if (graphqlUrl) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const sessionToken = urlParams.get('sessionToken');
|
||||
if (!sessionToken) {
|
||||
loadingContextInfo.setLoading(false, '');
|
||||
throw new Error('Missing session token');
|
||||
}
|
||||
sessionStorage.setItem('sessionToken', sessionToken);
|
||||
|
||||
let wsLink;
|
||||
try {
|
||||
const subscription = new SubscriptionClient(graphqlUrl, {
|
||||
reconnect: true,
|
||||
timeout: 30000,
|
||||
connectionParams: {
|
||||
headers: {
|
||||
'X-Session-Token': sessionToken,
|
||||
},
|
||||
},
|
||||
});
|
||||
subscription.onError(() => {
|
||||
loadingContextInfo.setLoading(false, '');
|
||||
throw new Error('Error: on subscription to server');
|
||||
});
|
||||
wsLink = new WebSocketLink(
|
||||
subscription,
|
||||
);
|
||||
wsLink.setOnError((error) => {
|
||||
loadingContextInfo.setLoading(false, '');
|
||||
throw new Error('Error: on apollo connection'.concat(JSON.stringify(error) || ''));
|
||||
});
|
||||
} catch (error) {
|
||||
loadingContextInfo.setLoading(false, '');
|
||||
throw new Error('Error creating WebSocketLink: '.concat(JSON.stringify(error) || ''));
|
||||
}
|
||||
let client;
|
||||
try {
|
||||
client = new ApolloClient({
|
||||
link: wsLink,
|
||||
cache: new InMemoryCache(),
|
||||
connectToDevTools: Meteor.isDevelopment,
|
||||
});
|
||||
setApolloClient(client);
|
||||
} catch (error) {
|
||||
loadingContextInfo.setLoading(false, '');
|
||||
throw new Error('Error creating Apollo Client: '.concat(JSON.stringify(error) || ''));
|
||||
}
|
||||
}
|
||||
},
|
||||
[graphqlUrl]);
|
||||
return (
|
||||
graphqlUrlApolloClient
|
||||
? (
|
||||
<ApolloProvider
|
||||
client={graphqlUrlApolloClient}
|
||||
>
|
||||
{children}
|
||||
</ApolloProvider>
|
||||
) : null
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionManager;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user