Merge pull request #18133 from GuiLeme/port-timer-with-events-suggestions
feat(timer): Port timer (Mconf) with events suggestions
This commit is contained in:
commit
c8b0437df3
@ -0,0 +1,100 @@
|
||||
package org.bigbluebutton.core.apps
|
||||
|
||||
object TimerModel {
|
||||
def createTimer(
|
||||
model: TimerModel,
|
||||
stopwatch: Boolean = true,
|
||||
time: Int = 0,
|
||||
accumulated: Int = 0,
|
||||
track: String = "",
|
||||
): Unit = {
|
||||
model.stopwatch = stopwatch
|
||||
model.time = time
|
||||
model.accumulated = accumulated
|
||||
model.track = track
|
||||
}
|
||||
|
||||
def reset(model: TimerModel, stopwatch: Boolean, time: Int, accumulated: Int, startedAt: Long, track: String) : Unit = {
|
||||
model.stopwatch = stopwatch
|
||||
model.time = time
|
||||
model.accumulated = accumulated
|
||||
model.startedAt = startedAt
|
||||
model.track = track
|
||||
model.endedAt = 0
|
||||
}
|
||||
|
||||
def setIsActive(model: TimerModel, active: Boolean): Unit = {
|
||||
model.isActive = active
|
||||
}
|
||||
|
||||
def getIsACtive(model: TimerModel): Boolean = {
|
||||
model.isActive
|
||||
}
|
||||
|
||||
def setStartedAt(model: TimerModel, timestamp: Long): Unit = {
|
||||
model.startedAt = timestamp
|
||||
}
|
||||
|
||||
def getStartedAt(model: TimerModel): Long = {
|
||||
model.startedAt
|
||||
}
|
||||
|
||||
def setAccumulated(model: TimerModel, accumulated: Int): Unit = {
|
||||
model.accumulated = accumulated
|
||||
}
|
||||
|
||||
def getAccumulated(model: TimerModel): Int = {
|
||||
model.accumulated
|
||||
}
|
||||
|
||||
def setRunning(model: TimerModel, running: Boolean): Unit = {
|
||||
model.running = running
|
||||
}
|
||||
|
||||
def getRunning(model: TimerModel): Boolean = {
|
||||
model.running
|
||||
}
|
||||
|
||||
def setStopwatch(model: TimerModel, stopwatch: Boolean): Unit = {
|
||||
model.stopwatch = stopwatch
|
||||
}
|
||||
|
||||
def getStopwatch(model: TimerModel): Boolean = {
|
||||
model.stopwatch
|
||||
}
|
||||
|
||||
def setTrack(model: TimerModel, track: String): Unit = {
|
||||
model.track = track
|
||||
}
|
||||
|
||||
def getTrack(model: TimerModel): String = {
|
||||
model.track
|
||||
}
|
||||
|
||||
def setTime(model: TimerModel, time: Int): Unit = {
|
||||
model.time = time
|
||||
}
|
||||
|
||||
def getTime(model: TimerModel): Int = {
|
||||
model.time
|
||||
}
|
||||
|
||||
def setEndedAt(model: TimerModel, timestamp: Long): Unit = {
|
||||
model.endedAt = timestamp
|
||||
}
|
||||
|
||||
def getEndedAt(model: TimerModel): Long = {
|
||||
model.endedAt
|
||||
}
|
||||
}
|
||||
|
||||
class TimerModel {
|
||||
private var startedAt: Long = 0
|
||||
private var endedAt: Long = 0
|
||||
private var accumulated: Int = 0
|
||||
private var running: Boolean = false
|
||||
private var time: Int = 0
|
||||
private var stopwatch: Boolean = true
|
||||
private var track: String = ""
|
||||
private var isActive: Boolean = false
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package org.bigbluebutton.core.apps.timer
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait }
|
||||
|
||||
trait ActivateTimerReqMsgHdlr extends RightsManagementTrait {
|
||||
this: TimerApp2x =>
|
||||
|
||||
def handle(msg: ActivateTimerReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
log.debug("Received ActivateTimerReqMsg {}", ActivateTimerReqMsg)
|
||||
def broadcastEvent(
|
||||
stopwatch: Boolean,
|
||||
running: Boolean,
|
||||
time: Int,
|
||||
accumulated: Int,
|
||||
track: String
|
||||
): Unit = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(ActivateTimerRespMsg.NAME, routing)
|
||||
val header = BbbCoreHeaderWithMeetingId(
|
||||
ActivateTimerRespMsg.NAME,
|
||||
liveMeeting.props.meetingProp.intId
|
||||
)
|
||||
val body = ActivateTimerRespMsgBody(msg.header.userId, stopwatch, running, time, accumulated, track)
|
||||
val event = ActivateTimerRespMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) &&
|
||||
permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "You need to be the presenter or moderator to activate timer"
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||
} else {
|
||||
TimerModel.reset(liveMeeting.timerModel, msg.body.stopwatch, msg.body.time, msg.body.accumulated, msg.body.timestamp, msg.body.track)
|
||||
TimerModel.setIsActive(liveMeeting.timerModel, true)
|
||||
broadcastEvent(msg.body.stopwatch, msg.body.running, msg.body.time, msg.body.accumulated, msg.body.track)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package org.bigbluebutton.core.apps.timer
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait }
|
||||
|
||||
trait CreateTimerPubMsgHdlr extends RightsManagementTrait {
|
||||
this: TimerApp2x =>
|
||||
|
||||
def handle(msg: CreateTimerPubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
log.debug("Received CreateTimerPubMsg {}", CreateTimerPubMsg)
|
||||
TimerModel.createTimer(liveMeeting.timerModel, msg.body.stopwatch, msg.body.time, msg.body.accumulated, msg.body.track)
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package org.bigbluebutton.core.apps.timer
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait }
|
||||
|
||||
trait DeactivateTimerReqMsgHdlr extends RightsManagementTrait {
|
||||
this: TimerApp2x =>
|
||||
|
||||
def handle(msg: DeactivateTimerReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
log.debug("Received deactivateTimerReqMsg {}", DeactivateTimerReqMsg)
|
||||
def broadcastEvent(): Unit = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(DeactivateTimerRespMsg.NAME, routing)
|
||||
val header = BbbCoreHeaderWithMeetingId(
|
||||
DeactivateTimerRespMsg.NAME,
|
||||
liveMeeting.props.meetingProp.intId
|
||||
)
|
||||
val body = DeactivateTimerRespMsgBody(msg.header.userId)
|
||||
val event = DeactivateTimerRespMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) &&
|
||||
permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "You need to be the presenter or moderator to deactivate timer"
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||
} else {
|
||||
TimerModel.setIsActive(liveMeeting.timerModel, false);
|
||||
broadcastEvent()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package org.bigbluebutton.core.apps.timer
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait }
|
||||
|
||||
trait ResetTimerReqMsgHdlr extends RightsManagementTrait {
|
||||
this: TimerApp2x =>
|
||||
|
||||
def handle(msg: ResetTimerReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
log.debug("Received resetTimerReqMsg {}", ResetTimerReqMsg)
|
||||
def broadcastEvent(): Unit = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(ResetTimerRespMsg.NAME, routing)
|
||||
val header = BbbCoreHeaderWithMeetingId(
|
||||
ResetTimerRespMsg.NAME,
|
||||
liveMeeting.props.meetingProp.intId
|
||||
)
|
||||
val body = ResetTimerRespMsgBody(msg.header.userId)
|
||||
val event = ResetTimerRespMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) &&
|
||||
permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "You need to be the presenter or moderator to reset timer"
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||
} else {
|
||||
broadcastEvent()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package org.bigbluebutton.core.apps.timer
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait }
|
||||
|
||||
trait SetTimerReqMsgHdlr extends RightsManagementTrait {
|
||||
this: TimerApp2x =>
|
||||
|
||||
def handle(msg: SetTimerReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
log.debug("Received setTimerReqMsg {}", SetTimerReqMsg)
|
||||
def broadcastEvent(
|
||||
time: Int
|
||||
): Unit = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(SetTimerRespMsg.NAME, routing)
|
||||
val header = BbbCoreHeaderWithMeetingId(
|
||||
SetTimerRespMsg.NAME,
|
||||
liveMeeting.props.meetingProp.intId
|
||||
)
|
||||
val body = SetTimerRespMsgBody(msg.header.userId, time)
|
||||
val event = SetTimerRespMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) &&
|
||||
permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "You need to be the presenter or moderator to set timer"
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||
} else {
|
||||
TimerModel.setTime(liveMeeting.timerModel, msg.body.time)
|
||||
broadcastEvent(msg.body.time)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package org.bigbluebutton.core.apps.timer
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait }
|
||||
|
||||
trait SetTrackReqMsgHdlr extends RightsManagementTrait {
|
||||
this: TimerApp2x =>
|
||||
|
||||
def handle(msg: SetTrackReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
log.debug("Received setTrackReqMsg {}", SetTrackReqMsg)
|
||||
def broadcastEvent(
|
||||
track: String
|
||||
): Unit = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(SetTrackRespMsg.NAME, routing)
|
||||
val header = BbbCoreHeaderWithMeetingId(
|
||||
SetTrackRespMsg.NAME,
|
||||
liveMeeting.props.meetingProp.intId
|
||||
)
|
||||
val body = SetTrackRespMsgBody(msg.header.userId, track)
|
||||
val event = SetTrackRespMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) &&
|
||||
permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "You need to be the presenter or moderator to set track"
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||
} else {
|
||||
TimerModel.setTrack(liveMeeting.timerModel, msg.body.track)
|
||||
broadcastEvent(msg.body.track)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package org.bigbluebutton.core.apps.timer
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait }
|
||||
|
||||
trait StartTimerReqMsgHdlr extends RightsManagementTrait {
|
||||
this: TimerApp2x =>
|
||||
|
||||
def handle(msg: StartTimerReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
log.debug("Received startTimerReqMsg {}", StartTimerReqMsg)
|
||||
def broadcastEvent(): Unit = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(StartTimerRespMsg.NAME, routing)
|
||||
val header = BbbCoreHeaderWithMeetingId(
|
||||
StartTimerRespMsg.NAME,
|
||||
liveMeeting.props.meetingProp.intId
|
||||
)
|
||||
val body = StartTimerRespMsgBody(msg.header.userId)
|
||||
val event = StartTimerRespMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) &&
|
||||
permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "You need to be the presenter or moderator to start timer"
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||
} else {
|
||||
TimerModel.setStartedAt(liveMeeting.timerModel, System.currentTimeMillis())
|
||||
TimerModel.setRunning(liveMeeting.timerModel, true)
|
||||
broadcastEvent()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package org.bigbluebutton.core.apps.timer
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait }
|
||||
|
||||
trait StopTimerReqMsgHdlr extends RightsManagementTrait {
|
||||
this: TimerApp2x =>
|
||||
|
||||
def handle(msg: StopTimerReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
log.debug("Received stopTimerReqMsg {}", StopTimerReqMsg)
|
||||
def broadcastEvent(
|
||||
accumulated: Int
|
||||
): Unit = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(StopTimerRespMsg.NAME, routing)
|
||||
val header = BbbCoreHeaderWithMeetingId(
|
||||
StopTimerRespMsg.NAME,
|
||||
liveMeeting.props.meetingProp.intId
|
||||
)
|
||||
val body = StopTimerRespMsgBody(msg.header.userId, accumulated)
|
||||
val event = StopTimerRespMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) &&
|
||||
permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId) &&
|
||||
msg.header.userId != "nodeJSapp") {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "You need to be the presenter or moderator to stop timer"
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||
} else {
|
||||
TimerModel.setAccumulated(liveMeeting.timerModel, msg.body.accumulated)
|
||||
TimerModel.setRunning(liveMeeting.timerModel, false)
|
||||
broadcastEvent(msg.body.accumulated)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package org.bigbluebutton.core.apps.timer
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait }
|
||||
|
||||
trait SwitchTimerReqMsgHdlr extends RightsManagementTrait {
|
||||
this: TimerApp2x =>
|
||||
|
||||
def handle(msg: SwitchTimerReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
log.debug("Received switchTimerReqMsg {}", SwitchTimerReqMsg)
|
||||
def broadcastEvent(
|
||||
stopwatch: Boolean
|
||||
): Unit = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(SwitchTimerRespMsg.NAME, routing)
|
||||
val header = BbbCoreHeaderWithMeetingId(
|
||||
SwitchTimerRespMsg.NAME,
|
||||
liveMeeting.props.meetingProp.intId
|
||||
)
|
||||
val body = SwitchTimerRespMsgBody(msg.header.userId, stopwatch)
|
||||
val event = SwitchTimerRespMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) &&
|
||||
permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "You need to be the presenter or moderator to switch timer"
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||
} else {
|
||||
if (TimerModel.getStopwatch(liveMeeting.timerModel) != msg.body.stopwatch) {
|
||||
TimerModel.setStopwatch(liveMeeting.timerModel, msg.body.stopwatch)
|
||||
broadcastEvent(msg.body.stopwatch)
|
||||
} else {
|
||||
log.debug("Timer is already in this stopwatch mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package org.bigbluebutton.core.apps.timer
|
||||
|
||||
import akka.actor.ActorContext
|
||||
import akka.event.Logging
|
||||
|
||||
class TimerApp2x(implicit val context: ActorContext)
|
||||
extends CreateTimerPubMsgHdlr
|
||||
with ActivateTimerReqMsgHdlr
|
||||
with DeactivateTimerReqMsgHdlr
|
||||
with StartTimerReqMsgHdlr
|
||||
with StopTimerReqMsgHdlr
|
||||
with SwitchTimerReqMsgHdlr
|
||||
with SetTimerReqMsgHdlr
|
||||
with ResetTimerReqMsgHdlr
|
||||
with SetTrackReqMsgHdlr
|
||||
with TimerEndedPubMsgHdlr {
|
||||
|
||||
val log = Logging(context.system, getClass)
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package org.bigbluebutton.core.apps.timer
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait }
|
||||
|
||||
trait TimerEndedPubMsgHdlr extends RightsManagementTrait {
|
||||
this: TimerApp2x =>
|
||||
|
||||
def handle(msg: TimerEndedPubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
log.debug("Received timerEndedPubMsg {}", TimerEndedPubMsg)
|
||||
def broadcastEvent(): Unit = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(TimerEndedEvtMsg.NAME, routing)
|
||||
val header = BbbCoreHeaderWithMeetingId(
|
||||
TimerEndedEvtMsg.NAME,
|
||||
liveMeeting.props.meetingProp.intId
|
||||
)
|
||||
val body = TimerEndedEvtMsgBody()
|
||||
val event = TimerEndedEvtMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
TimerModel.setEndedAt(liveMeeting.timerModel, System.currentTimeMillis())
|
||||
broadcastEvent()
|
||||
}
|
||||
}
|
@ -408,6 +408,29 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeGenericMsg[UpdateExternalVideoPubMsg](envelope, jsonNode)
|
||||
case StopExternalVideoPubMsg.NAME =>
|
||||
routeGenericMsg[StopExternalVideoPubMsg](envelope, jsonNode)
|
||||
|
||||
// Timer
|
||||
case CreateTimerPubMsg.NAME =>
|
||||
routeGenericMsg[CreateTimerPubMsg](envelope, jsonNode)
|
||||
case ActivateTimerReqMsg.NAME =>
|
||||
routeGenericMsg[ActivateTimerReqMsg](envelope, jsonNode)
|
||||
case DeactivateTimerReqMsg.NAME =>
|
||||
routeGenericMsg[DeactivateTimerReqMsg](envelope, jsonNode)
|
||||
case StartTimerReqMsg.NAME =>
|
||||
routeGenericMsg[StartTimerReqMsg](envelope, jsonNode)
|
||||
case StopTimerReqMsg.NAME =>
|
||||
routeGenericMsg[StopTimerReqMsg](envelope, jsonNode)
|
||||
case SwitchTimerReqMsg.NAME =>
|
||||
routeGenericMsg[SwitchTimerReqMsg](envelope, jsonNode)
|
||||
case SetTimerReqMsg.NAME =>
|
||||
routeGenericMsg[SetTimerReqMsg](envelope, jsonNode)
|
||||
case ResetTimerReqMsg.NAME =>
|
||||
routeGenericMsg[ResetTimerReqMsg](envelope, jsonNode)
|
||||
case SetTrackReqMsg.NAME =>
|
||||
routeGenericMsg[SetTrackReqMsg](envelope, jsonNode)
|
||||
case TimerEndedPubMsg.NAME =>
|
||||
routeGenericMsg[TimerEndedPubMsg](envelope, jsonNode)
|
||||
|
||||
case _ =>
|
||||
log.error("Cannot route envelope name " + envelope.name)
|
||||
// do nothing
|
||||
|
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2019 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
trait AbstractTimerRecordEvent extends RecordEvent {
|
||||
setModule("TIMER")
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
class ActivateTimerRecordEvent extends AbstractTimerRecordEvent {
|
||||
import ActivateTimerRecordEvent._
|
||||
|
||||
setEvent("ActivateTimerEvent")
|
||||
|
||||
def setStopwatch(value: Boolean) {
|
||||
eventMap.put(STOPWATCH, value.toString)
|
||||
}
|
||||
|
||||
def setRunning(value: Boolean) {
|
||||
eventMap.put(RUNNING, value.toString)
|
||||
}
|
||||
|
||||
def setTime(value: Int) {
|
||||
eventMap.put(TIME, value.toString)
|
||||
}
|
||||
|
||||
def setAccumulated(value: Int) {
|
||||
eventMap.put(ACCUMULATED, value.toString)
|
||||
}
|
||||
|
||||
def setTimestamp(value: Int) {
|
||||
eventMap.put(TIMESTAMP, value.toString)
|
||||
}
|
||||
|
||||
def setTrack(value: String) {
|
||||
eventMap.put(TRACK, value)
|
||||
}
|
||||
}
|
||||
|
||||
object ActivateTimerRecordEvent {
|
||||
protected final val STOPWATCH = "stopwatch"
|
||||
protected final val RUNNING = "running"
|
||||
protected final val TIME = "time"
|
||||
protected final val ACCUMULATED = "accumulated"
|
||||
protected final val TIMESTAMP = "timestamp"
|
||||
protected final val TRACK = "track"
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
class DeactivateTimerRecordEvent extends AbstractTimerRecordEvent {
|
||||
setEvent("DeactivateTimerEvent")
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
class ResetTimerRecordEvent extends AbstractTimerRecordEvent {
|
||||
setEvent("ResetTimerEvent")
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
class SetTimerRecordEvent extends AbstractTimerRecordEvent {
|
||||
import SetTimerRecordEvent._
|
||||
|
||||
setEvent("SetTimerEvent")
|
||||
|
||||
def setTime(value: Int) {
|
||||
eventMap.put(TIME, value.toString)
|
||||
}
|
||||
}
|
||||
|
||||
object SetTimerRecordEvent {
|
||||
protected final val TIME = "time"
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
class SetTimerTrackRecordEvent extends AbstractTimerRecordEvent {
|
||||
import SetTimerTrackRecordEvent._
|
||||
|
||||
setEvent("SetTimerTrackEvent")
|
||||
|
||||
def setTrack(value: String) {
|
||||
eventMap.put(TRACK, value)
|
||||
}
|
||||
}
|
||||
|
||||
object SetTimerTrackRecordEvent {
|
||||
protected final val TRACK = "track"
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
class StartTimerRecordEvent extends AbstractTimerRecordEvent {
|
||||
setEvent("StartTimerEvent")
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
class StopTimerRecordEvent extends AbstractTimerRecordEvent {
|
||||
import StopTimerRecordEvent._
|
||||
|
||||
setEvent("StopTimerEvent")
|
||||
|
||||
def setAccumulated(value: Int) {
|
||||
eventMap.put(ACCUMULATED, value.toString)
|
||||
}
|
||||
}
|
||||
|
||||
object StopTimerRecordEvent {
|
||||
protected final val ACCUMULATED = "accumulated"
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
class SwitchTimerRecordEvent extends AbstractTimerRecordEvent {
|
||||
import SwitchTimerRecordEvent._
|
||||
|
||||
setEvent("SwitchTimerEvent")
|
||||
|
||||
def setStopwatch(value: Boolean) {
|
||||
eventMap.put(STOPWATCH, value.toString)
|
||||
}
|
||||
}
|
||||
|
||||
object SwitchTimerRecordEvent {
|
||||
protected final val STOPWATCH = "stopwatch"
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
class TimerEndedRecordEvent extends AbstractTimerRecordEvent {
|
||||
setEvent("TimerEndedEvent")
|
||||
}
|
@ -10,6 +10,7 @@ class LiveMeeting(
|
||||
val status: MeetingStatus2x,
|
||||
val screenshareModel: ScreenshareModel,
|
||||
val audioCaptions: AudioCaptions,
|
||||
val timerModel: TimerModel,
|
||||
val chatModel: ChatModel,
|
||||
val externalVideoModel: ExternalVideoModel,
|
||||
val layouts: Layouts,
|
||||
|
@ -19,6 +19,7 @@ import org.bigbluebutton.core.apps.externalvideo.ExternalVideoApp2x
|
||||
import org.bigbluebutton.core.apps.pads.PadsApp2x
|
||||
import org.bigbluebutton.core.apps.screenshare.ScreenshareApp2x
|
||||
import org.bigbluebutton.core.apps.audiocaptions.AudioCaptionsApp2x
|
||||
import org.bigbluebutton.core.apps.timer.TimerApp2x
|
||||
import org.bigbluebutton.core.apps.presentation.PresentationApp2x
|
||||
import org.bigbluebutton.core.apps.users.UsersApp2x
|
||||
import org.bigbluebutton.core.apps.webcam.WebcamApp2x
|
||||
@ -136,6 +137,7 @@ class MeetingActor(
|
||||
val pollApp = new PollApp2x
|
||||
val webcamApp2x = new WebcamApp2x
|
||||
val wbApp = new WhiteboardApp2x
|
||||
val timerApp2x = new TimerApp2x
|
||||
|
||||
object ExpiryTrackerHelper extends MeetingExpiryTrackerHelper
|
||||
|
||||
@ -599,6 +601,18 @@ class MeetingActor(
|
||||
case m: UpdateExternalVideoPubMsg => externalVideoApp2x.handle(m, liveMeeting, msgBus)
|
||||
case m: StopExternalVideoPubMsg => externalVideoApp2x.handle(m, liveMeeting, msgBus)
|
||||
|
||||
//Timer
|
||||
case m: CreateTimerPubMsg => timerApp2x.handle(m, liveMeeting, msgBus)
|
||||
case m: ActivateTimerReqMsg => timerApp2x.handle(m, liveMeeting, msgBus)
|
||||
case m: DeactivateTimerReqMsg => timerApp2x.handle(m, liveMeeting, msgBus)
|
||||
case m: StartTimerReqMsg => timerApp2x.handle(m, liveMeeting, msgBus)
|
||||
case m: StopTimerReqMsg => timerApp2x.handle(m, liveMeeting, msgBus)
|
||||
case m: SwitchTimerReqMsg => timerApp2x.handle(m, liveMeeting, msgBus)
|
||||
case m: SetTimerReqMsg => timerApp2x.handle(m, liveMeeting, msgBus)
|
||||
case m: ResetTimerReqMsg => timerApp2x.handle(m, liveMeeting, msgBus)
|
||||
case m: SetTrackReqMsg => timerApp2x.handle(m, liveMeeting, msgBus)
|
||||
case m: TimerEndedPubMsg => timerApp2x.handle(m, liveMeeting, msgBus)
|
||||
|
||||
case m: ValidateConnAuthTokenSysMsg => handleValidateConnAuthTokenSysMsg(m)
|
||||
|
||||
case m: UserActivitySignCmdMsg => handleUserActivitySignCmdMsg(m)
|
||||
|
@ -33,13 +33,14 @@ class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway,
|
||||
private val guestsWaiting = new GuestsWaiting
|
||||
private val deskshareModel = new ScreenshareModel
|
||||
private val audioCaptions = new AudioCaptions
|
||||
private val timerModel = new TimerModel
|
||||
|
||||
// meetingModel.setGuestPolicy(props.usersProp.guestPolicy)
|
||||
|
||||
// We extract the meeting handlers into this class so it is
|
||||
// easy to test.
|
||||
private val liveMeeting = new LiveMeeting(props, meetingStatux2x, deskshareModel, audioCaptions, chatModel, externalVideoModel,
|
||||
layouts, pads, registeredUsers, polls2x, wbModel, presModel, captionModel,
|
||||
private val liveMeeting = new LiveMeeting(props, meetingStatux2x, deskshareModel, audioCaptions, timerModel,
|
||||
chatModel, externalVideoModel, layouts, pads, registeredUsers, polls2x, wbModel, presModel, captionModel,
|
||||
webcams, voiceUsers, users2x, guestsWaiting)
|
||||
|
||||
GuestsWaiting.setGuestPolicy(
|
||||
|
@ -135,6 +135,17 @@ class RedisRecorderActor(
|
||||
case m: UpdateExternalVideoEvtMsg => handleUpdateExternalVideoEvtMsg(m)
|
||||
case m: StopExternalVideoEvtMsg => handleStopExternalVideoEvtMsg(m)
|
||||
|
||||
// Timer
|
||||
case m: ActivateTimerRespMsg => handleActivateTimerRespMsg(m)
|
||||
case m: DeactivateTimerRespMsg => handleDeactivateTimerRespMsg(m)
|
||||
case m: StartTimerRespMsg => handleStartTimerRespMsg(m)
|
||||
case m: StopTimerRespMsg => handleStopTimerRespMsg(m)
|
||||
case m: SwitchTimerRespMsg => handleSwitchTimerRespMsg(m)
|
||||
case m: SetTimerRespMsg => handleSetTimerRespMsg(m)
|
||||
case m: ResetTimerRespMsg => handleResetTimerRespMsg(m)
|
||||
case m: TimerEndedEvtMsg => handleTimerEndedEvtMsg(m)
|
||||
case m: SetTrackRespMsg => handleSetTrackRespMsg(m)
|
||||
|
||||
case _ => // message not to be recorded.
|
||||
}
|
||||
}
|
||||
@ -545,6 +556,78 @@ class RedisRecorderActor(
|
||||
record(msg.header.meetingId, ev.toMap.asJava)
|
||||
}
|
||||
|
||||
private def handleActivateTimerRespMsg(msg: ActivateTimerRespMsg) {
|
||||
val ev = new ActivateTimerRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
ev.setStopwatch(msg.body.stopwatch)
|
||||
ev.setRunning(msg.body.running)
|
||||
ev.setTime(msg.body.time)
|
||||
ev.setAccumulated(msg.body.accumulated)
|
||||
ev.setTrack(msg.body.track)
|
||||
|
||||
record(msg.header.meetingId, ev.toMap.asJava)
|
||||
}
|
||||
|
||||
private def handleDeactivateTimerRespMsg(msg: DeactivateTimerRespMsg) {
|
||||
val ev = new DeactivateTimerRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
|
||||
record(msg.header.meetingId, ev.toMap.asJava)
|
||||
}
|
||||
|
||||
private def handleStartTimerRespMsg(msg: StartTimerRespMsg) {
|
||||
val ev = new StartTimerRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
|
||||
record(msg.header.meetingId, ev.toMap.asJava)
|
||||
}
|
||||
|
||||
private def handleStopTimerRespMsg(msg: StopTimerRespMsg) {
|
||||
val ev = new StopTimerRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
ev.setAccumulated(msg.body.accumulated)
|
||||
|
||||
record(msg.header.meetingId, ev.toMap.asJava)
|
||||
}
|
||||
|
||||
private def handleSwitchTimerRespMsg(msg: SwitchTimerRespMsg) {
|
||||
val ev = new SwitchTimerRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
ev.setStopwatch(msg.body.stopwatch)
|
||||
|
||||
record(msg.header.meetingId, ev.toMap.asJava)
|
||||
}
|
||||
|
||||
private def handleSetTimerRespMsg(msg: SetTimerRespMsg) {
|
||||
val ev = new SetTimerRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
ev.setTime(msg.body.time)
|
||||
|
||||
record(msg.header.meetingId, ev.toMap.asJava)
|
||||
}
|
||||
|
||||
private def handleResetTimerRespMsg(msg: ResetTimerRespMsg) {
|
||||
val ev = new ResetTimerRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
|
||||
record(msg.header.meetingId, ev.toMap.asJava)
|
||||
}
|
||||
|
||||
private def handleTimerEndedEvtMsg(msg: TimerEndedEvtMsg) {
|
||||
val ev = new TimerEndedRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
|
||||
record(msg.header.meetingId, ev.toMap.asJava)
|
||||
}
|
||||
|
||||
private def handleSetTrackRespMsg(msg: SetTrackRespMsg) {
|
||||
val ev = new SetTimerTrackRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
ev.setTrack(msg.body.track)
|
||||
|
||||
record(msg.header.meetingId, ev.toMap.asJava)
|
||||
}
|
||||
|
||||
private def handleRecordingStatusChangedEvtMsg(msg: RecordingStatusChangedEvtMsg) {
|
||||
val ev = new RecordStatusRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
|
@ -0,0 +1,79 @@
|
||||
package org.bigbluebutton.common2.msgs
|
||||
|
||||
/* In Messages */
|
||||
object CreateTimerPubMsg { val NAME = "CreateTimerPubMsg" }
|
||||
case class CreateTimerPubMsg(header: BbbClientMsgHeader, body: CreateTimerPubMsgBody) extends StandardMsg
|
||||
case class CreateTimerPubMsgBody(stopwatch: Boolean, running: Boolean, time: Int, accumulated: Int, timestamp: Int, track: String)
|
||||
|
||||
object ActivateTimerReqMsg { val NAME = "ActivateTimerReqMsg" }
|
||||
case class ActivateTimerReqMsg(header: BbbClientMsgHeader, body: ActivateTimerReqMsgBody) extends StandardMsg
|
||||
case class ActivateTimerReqMsgBody(stopwatch: Boolean, running: Boolean, time: Int, accumulated: Int, timestamp: Int, track: String)
|
||||
|
||||
object DeactivateTimerReqMsg { val NAME = "DeactivateTimerReqMsg" }
|
||||
case class DeactivateTimerReqMsg(header: BbbClientMsgHeader, body: DeactivateTimerReqMsgBody) extends StandardMsg
|
||||
case class DeactivateTimerReqMsgBody()
|
||||
|
||||
object StartTimerReqMsg { val NAME = "StartTimerReqMsg" }
|
||||
case class StartTimerReqMsg(header: BbbClientMsgHeader, body: StartTimerReqMsgBody) extends StandardMsg
|
||||
case class StartTimerReqMsgBody()
|
||||
|
||||
object StopTimerReqMsg { val NAME = "StopTimerReqMsg" }
|
||||
case class StopTimerReqMsg(header: BbbClientMsgHeader, body: StopTimerReqMsgBody) extends StandardMsg
|
||||
case class StopTimerReqMsgBody(accumulated: Int)
|
||||
|
||||
object SwitchTimerReqMsg { val NAME = "SwitchTimerReqMsg" }
|
||||
case class SwitchTimerReqMsg(header: BbbClientMsgHeader, body: SwitchTimerReqMsgBody) extends StandardMsg
|
||||
case class SwitchTimerReqMsgBody(stopwatch: Boolean)
|
||||
|
||||
object SetTimerReqMsg { val NAME = "SetTimerReqMsg" }
|
||||
case class SetTimerReqMsg(header: BbbClientMsgHeader, body: SetTimerReqMsgBody) extends StandardMsg
|
||||
case class SetTimerReqMsgBody(time: Int)
|
||||
|
||||
object ResetTimerReqMsg { val NAME = "ResetTimerReqMsg" }
|
||||
case class ResetTimerReqMsg(header: BbbClientMsgHeader, body: ResetTimerReqMsgBody) extends StandardMsg
|
||||
case class ResetTimerReqMsgBody()
|
||||
|
||||
object TimerEndedPubMsg { val NAME = "TimerEndedPubMsg" }
|
||||
case class TimerEndedPubMsg(header: BbbClientMsgHeader, body: TimerEndedPubMsgBody) extends StandardMsg
|
||||
case class TimerEndedPubMsgBody()
|
||||
|
||||
object SetTrackReqMsg { val NAME = "SetTrackReqMsg" }
|
||||
case class SetTrackReqMsg(header: BbbClientMsgHeader, body: SetTrackReqMsgBody) extends StandardMsg
|
||||
case class SetTrackReqMsgBody(track: String)
|
||||
|
||||
/* Out Messages */
|
||||
object ActivateTimerRespMsg { val NAME = "ActivateTimerRespMsg" }
|
||||
case class ActivateTimerRespMsg(header: BbbCoreHeaderWithMeetingId, body: ActivateTimerRespMsgBody) extends BbbCoreMsg
|
||||
case class ActivateTimerRespMsgBody(userId: String, stopwatch: Boolean, running: Boolean, time: Int, accumulated: Int, track: String)
|
||||
|
||||
object DeactivateTimerRespMsg { val NAME = "DeactivateTimerRespMsg" }
|
||||
case class DeactivateTimerRespMsg(header: BbbCoreHeaderWithMeetingId, body: DeactivateTimerRespMsgBody) extends BbbCoreMsg
|
||||
case class DeactivateTimerRespMsgBody(userId: String)
|
||||
|
||||
object StartTimerRespMsg { val NAME = "StartTimerRespMsg" }
|
||||
case class StartTimerRespMsg(header: BbbCoreHeaderWithMeetingId, body: StartTimerRespMsgBody) extends BbbCoreMsg
|
||||
case class StartTimerRespMsgBody(userId: String)
|
||||
|
||||
object StopTimerRespMsg { val NAME = "StopTimerRespMsg" }
|
||||
case class StopTimerRespMsg(header: BbbCoreHeaderWithMeetingId, body: StopTimerRespMsgBody) extends BbbCoreMsg
|
||||
case class StopTimerRespMsgBody(userId: String, accumulated: Int)
|
||||
|
||||
object SwitchTimerRespMsg { val NAME = "SwitchTimerRespMsg" }
|
||||
case class SwitchTimerRespMsg(header: BbbCoreHeaderWithMeetingId, body: SwitchTimerRespMsgBody) extends BbbCoreMsg
|
||||
case class SwitchTimerRespMsgBody(userId: String, stopwatch: Boolean)
|
||||
|
||||
object SetTimerRespMsg { val NAME = "SetTimerRespMsg" }
|
||||
case class SetTimerRespMsg(header: BbbCoreHeaderWithMeetingId, body: SetTimerRespMsgBody) extends BbbCoreMsg
|
||||
case class SetTimerRespMsgBody(userId: String, time: Int)
|
||||
|
||||
object ResetTimerRespMsg { val NAME = "ResetTimerRespMsg" }
|
||||
case class ResetTimerRespMsg(header: BbbCoreHeaderWithMeetingId, body: ResetTimerRespMsgBody) extends BbbCoreMsg
|
||||
case class ResetTimerRespMsgBody(userId: String)
|
||||
|
||||
object TimerEndedEvtMsg { val NAME = "TimerEndedEvtMsg" }
|
||||
case class TimerEndedEvtMsg(header: BbbCoreHeaderWithMeetingId, body: TimerEndedEvtMsgBody) extends BbbCoreMsg
|
||||
case class TimerEndedEvtMsgBody()
|
||||
|
||||
object SetTrackRespMsg { val NAME = "SetTrackRespMsg" }
|
||||
case class SetTrackRespMsg(header: BbbCoreHeaderWithMeetingId, body: SetTrackRespMsgBody) extends BbbCoreMsg
|
||||
case class SetTrackRespMsgBody(userId: String, track: String)
|
@ -11,6 +11,7 @@ import Meetings, {
|
||||
} from '/imports/api/meetings';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { initPads } from '/imports/api/pads/server/helpers';
|
||||
import createTimer from '/imports/api/timer/server/methods/createTimer';
|
||||
import { initCaptions } from '/imports/api/captions/server/helpers';
|
||||
import { addAnnotationsStreamer } from '/imports/api/annotations/server/streamer';
|
||||
import { addCursorStreamer } from '/imports/api/cursor/server/streamer';
|
||||
@ -264,6 +265,8 @@ export default async function addMeeting(meeting) {
|
||||
|
||||
if (insertedId) {
|
||||
Logger.info(`Added meeting id=${meetingId}`);
|
||||
// Init Timer collection
|
||||
createTimer(meetingId);
|
||||
if (newMeeting.meetingProp.disabledFeatures.indexOf('sharedNotes') === -1) {
|
||||
initPads(meetingId);
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import clearVoiceUsers from '/imports/api/voice-users/server/modifiers/clearVoic
|
||||
import clearUserInfo from '/imports/api/users-infos/server/modifiers/clearUserInfo';
|
||||
import clearConnectionStatus from '/imports/api/connection-status/server/modifiers/clearConnectionStatus';
|
||||
import clearScreenshare from '/imports/api/screenshare/server/modifiers/clearScreenshare';
|
||||
import clearTimer from '/imports/api/timer/server/modifiers/clearTimer';
|
||||
import clearAudioCaptions from '/imports/api/audio-captions/server/modifiers/clearAudioCaptions';
|
||||
import clearMeetingTimeRemaining from '/imports/api/meetings/server/modifiers/clearMeetingTimeRemaining';
|
||||
import clearLocalSettings from '/imports/api/local-settings/server/modifiers/clearLocalSettings';
|
||||
@ -57,6 +58,7 @@ export default async function meetingHasEnded(meetingId) {
|
||||
clearVoiceUsers(meetingId),
|
||||
clearUserInfo(meetingId),
|
||||
clearConnectionStatus(meetingId),
|
||||
clearTimer(meetingId),
|
||||
clearAudioCaptions(meetingId),
|
||||
clearLocalSettings(meetingId),
|
||||
clearMeetingTimeRemaining(meetingId),
|
||||
|
9
bigbluebutton-html5/imports/api/timer/index.js
Normal file
9
bigbluebutton-html5/imports/api/timer/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
const Timer = new Mongo.Collection('timer');
|
||||
|
||||
if (Meteor.isServer) {
|
||||
Timer.createIndex({ meetingId: 1 });
|
||||
}
|
||||
|
||||
export default Timer;
|
@ -0,0 +1,20 @@
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import handleTimerActivated from './handlers/timerActivated';
|
||||
import handleTimerDeactivated from './handlers/timerDeactivated';
|
||||
import handleTimerStarted from './handlers/timerStarted';
|
||||
import handleTimerStopped from './handlers/timerStopped';
|
||||
import handleTimerSwitched from './handlers/timerSwitched';
|
||||
import handleTimerSet from './handlers/timerSet';
|
||||
import handleTimerReset from './handlers/timerReset';
|
||||
import handleTimerEnded from './handlers/timerEnded';
|
||||
import handleTrackSet from './handlers/trackSet';
|
||||
|
||||
RedisPubSub.on('ActivateTimerRespMsg', handleTimerActivated);
|
||||
RedisPubSub.on('DeactivateTimerRespMsg', handleTimerDeactivated);
|
||||
RedisPubSub.on('StartTimerRespMsg', handleTimerStarted);
|
||||
RedisPubSub.on('StopTimerRespMsg', handleTimerStopped);
|
||||
RedisPubSub.on('SwitchTimerRespMsg', handleTimerSwitched);
|
||||
RedisPubSub.on('SetTimerRespMsg', handleTimerSet);
|
||||
RedisPubSub.on('ResetTimerRespMsg', handleTimerReset);
|
||||
RedisPubSub.on('TimerEndedEvtMsg', handleTimerEnded);
|
||||
RedisPubSub.on('SetTrackRespMsg', handleTrackSet);
|
@ -0,0 +1,14 @@
|
||||
import { check } from 'meteor/check';
|
||||
import updateTimer from '/imports/api/timer/server/modifiers/updateTimer';
|
||||
|
||||
export default function handleTimerActivated({ body }, meetingId) {
|
||||
const { userId } = body;
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
|
||||
updateTimer({
|
||||
action: 'activate',
|
||||
meetingId,
|
||||
requesterUserId: userId,
|
||||
});
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { check } from 'meteor/check';
|
||||
import updateTimer from '/imports/api/timer/server/modifiers/updateTimer';
|
||||
|
||||
export default function handleTimerDeactivated({ body }, meetingId) {
|
||||
const { userId } = body;
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
|
||||
updateTimer({
|
||||
action: 'deactivate',
|
||||
meetingId,
|
||||
requesterUserId: userId,
|
||||
});
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { check } from 'meteor/check';
|
||||
import updateTimer from '/imports/api/timer/server/modifiers/updateTimer';
|
||||
|
||||
export default function handleTimerEnded({ body }, meetingId) {
|
||||
check(meetingId, String);
|
||||
check(body, Object);
|
||||
|
||||
updateTimer({
|
||||
action: 'reset',
|
||||
meetingId,
|
||||
requesterUserId: 'nodeJSapp',
|
||||
});
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { check } from 'meteor/check';
|
||||
import updateTimer from '/imports/api/timer/server/modifiers/updateTimer';
|
||||
|
||||
export default function handleTimerReset({ body }, meetingId) {
|
||||
const { userId } = body;
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
|
||||
updateTimer({
|
||||
action: 'reset',
|
||||
meetingId,
|
||||
requesterUserId: userId,
|
||||
});
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { check } from 'meteor/check';
|
||||
import updateTimer from '/imports/api/timer/server/modifiers/updateTimer';
|
||||
|
||||
export default function handleTimerSet({ body }, meetingId) {
|
||||
const { userId, time } = body;
|
||||
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
check(time, Number);
|
||||
|
||||
updateTimer({
|
||||
action: 'set',
|
||||
meetingId,
|
||||
requesterUserId: userId,
|
||||
time,
|
||||
});
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { check } from 'meteor/check';
|
||||
import updateTimer from '/imports/api/timer/server/modifiers/updateTimer';
|
||||
|
||||
export default function handleTimerStarted({ body }, meetingId) {
|
||||
const { userId } = body;
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
|
||||
updateTimer({
|
||||
action: 'start',
|
||||
meetingId,
|
||||
requesterUserId: userId,
|
||||
});
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { check } from 'meteor/check';
|
||||
import updateTimer from '/imports/api/timer/server/modifiers/updateTimer';
|
||||
|
||||
export default function handleTimerStopped({ body }, meetingId) {
|
||||
const { userId, accumulated } = body;
|
||||
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
check(accumulated, Number);
|
||||
|
||||
updateTimer({
|
||||
action: 'stop',
|
||||
meetingId,
|
||||
requesterUserId: userId,
|
||||
accumulated,
|
||||
});
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { check } from 'meteor/check';
|
||||
import updateTimer from '/imports/api/timer/server/modifiers/updateTimer';
|
||||
|
||||
export default function handleTimerSwitched({ body }, meetingId) {
|
||||
const { userId, stopwatch } = body;
|
||||
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
check(stopwatch, Boolean);
|
||||
|
||||
updateTimer({
|
||||
action: 'switch',
|
||||
meetingId,
|
||||
requesterUserId: userId,
|
||||
stopwatch,
|
||||
});
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { check } from 'meteor/check';
|
||||
import updateTimer from '/imports/api/timer/server/modifiers/updateTimer';
|
||||
|
||||
export default function handleTrackSet({ body }, meetingId) {
|
||||
const { userId, track } = body;
|
||||
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
check(track, String);
|
||||
|
||||
updateTimer({
|
||||
action: 'track',
|
||||
meetingId,
|
||||
requesterUserId: userId,
|
||||
stopwatch: false,
|
||||
track,
|
||||
});
|
||||
}
|
40
bigbluebutton-html5/imports/api/timer/server/helpers.js
Normal file
40
bigbluebutton-html5/imports/api/timer/server/helpers.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
const TIMER_CONFIG = Meteor.settings.public.timer;
|
||||
|
||||
const MILLI_IN_MINUTE = 60000;
|
||||
|
||||
const TRACKS = [
|
||||
'noTrack',
|
||||
'track1',
|
||||
'track2',
|
||||
'track3',
|
||||
];
|
||||
|
||||
const isEnabled = () => TIMER_CONFIG.enabled;
|
||||
|
||||
const getDefaultTime = () => TIMER_CONFIG.time * MILLI_IN_MINUTE;
|
||||
|
||||
const getInitialState = () => {
|
||||
const time = getDefaultTime();
|
||||
check(time, Number);
|
||||
|
||||
return {
|
||||
stopwatch: true,
|
||||
running: false,
|
||||
time,
|
||||
accumulated: 0,
|
||||
timestamp: 0,
|
||||
track: TRACKS[0],
|
||||
};
|
||||
};
|
||||
|
||||
const isTrackValid = (track) => TRACKS.includes(track);
|
||||
|
||||
export {
|
||||
TRACKS,
|
||||
isEnabled,
|
||||
getDefaultTime,
|
||||
getInitialState,
|
||||
isTrackValid,
|
||||
};
|
3
bigbluebutton-html5/imports/api/timer/server/index.js
Normal file
3
bigbluebutton-html5/imports/api/timer/server/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import './methods';
|
||||
import './eventHandlers';
|
||||
import './publishers';
|
24
bigbluebutton-html5/imports/api/timer/server/methods.js
Normal file
24
bigbluebutton-html5/imports/api/timer/server/methods.js
Normal file
@ -0,0 +1,24 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import activateTimer from './methods/activateTimer';
|
||||
import deactivateTimer from './methods/deactivateTimer';
|
||||
import resetTimer from './methods/resetTimer';
|
||||
import startTimer from './methods/startTimer';
|
||||
import stopTimer from './methods/stopTimer';
|
||||
import switchTimer from './methods/switchTimer';
|
||||
import setTimer from './methods/setTimer';
|
||||
import getServerTime from './methods/getServerTime';
|
||||
import setTrack from './methods/setTrack';
|
||||
import timerEnded from './methods/endTimer';
|
||||
|
||||
Meteor.methods({
|
||||
activateTimer,
|
||||
deactivateTimer,
|
||||
resetTimer,
|
||||
startTimer,
|
||||
stopTimer,
|
||||
switchTimer,
|
||||
setTimer,
|
||||
getServerTime,
|
||||
setTrack,
|
||||
timerEnded,
|
||||
});
|
@ -0,0 +1,23 @@
|
||||
import { check } from 'meteor/check';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import { getInitialState } from '../helpers';
|
||||
|
||||
export default function activateTimer() {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'ActivateTimerReqMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
const payload = getInitialState();
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
} catch (err) {
|
||||
Logger.error(`Activating timer: ${err}`);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import { check } from 'meteor/check';
|
||||
import { getInitialState, isEnabled } from '/imports/api/timer/server/helpers';
|
||||
import addTimer from '/imports/api/timer/server/modifiers/addTimer';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
// This method should only be used by the server
|
||||
export default function createTimer(meetingId) {
|
||||
check(meetingId, String);
|
||||
|
||||
// Avoid timer creation if this feature is disabled
|
||||
if (!isEnabled()) {
|
||||
Logger.warn(`Timers are disabled for ${meetingId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'CreateTimerPubMsg';
|
||||
|
||||
try {
|
||||
addTimer(meetingId);
|
||||
const payload = getInitialState();
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, 'nodeJsApp', payload);
|
||||
} catch (err) {
|
||||
Logger.error(`Activating timer: ${err}`);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { check } from 'meteor/check';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
|
||||
export default function deactivateTimer() {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'DeactivateTimerReqMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, {});
|
||||
} catch (err) {
|
||||
Logger.error(`Deactivating timer: ${err}`);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import { check } from 'meteor/check';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import updateTimer from '/imports/api/timer/server/modifiers/updateTimer';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
|
||||
export default function timerEnded() {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
updateTimer({
|
||||
action: 'ended',
|
||||
meetingId,
|
||||
requesterUserId,
|
||||
});
|
||||
}
|
||||
|
||||
// This method should only be used by the server
|
||||
export function sysEndTimer(meetingId) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'TimerEndedPubMsg';
|
||||
const USER_ID = 'nodeJSapp';
|
||||
|
||||
try {
|
||||
check(meetingId, String);
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, USER_ID, {});
|
||||
} catch (err) {
|
||||
Logger.error(`Ending timer: ${err}`);
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
export default function getServerTime() {
|
||||
if (this.userId) return Date.now();
|
||||
|
||||
return 0;
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { check } from 'meteor/check';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
|
||||
export default function resetTimer() {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'ResetTimerReqMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, {});
|
||||
} catch (err) {
|
||||
Logger.error(`Resetting timer: ${err}`);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { check } from 'meteor/check';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
|
||||
export default function setTimer(time) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'SetTimerReqMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(time, Number);
|
||||
|
||||
const payload = {
|
||||
time,
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
} catch (err) {
|
||||
Logger.error(`Setting timer: ${err}`);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { check } from 'meteor/check';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import { isTrackValid } from '/imports/api/timer/server/helpers';
|
||||
|
||||
export default function setTrack(track) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'SetTrackReqMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(track, String);
|
||||
|
||||
if (isTrackValid(track)) {
|
||||
const payload = {
|
||||
track,
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
} else {
|
||||
Logger.warn(`User=${requesterUserId} tried to set invalid track '${track}' in meeting=${meetingId}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Setting track: ${err}`);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { check } from 'meteor/check';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
|
||||
export default function startTimer() {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'StartTimerReqMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, {});
|
||||
} catch (err) {
|
||||
Logger.error(`Starting timer: ${err}`);
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Timer from '/imports/api/timer';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
|
||||
export default function stopTimer() {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'StopTimerReqMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
const now = Date.now();
|
||||
const timer = Timer.findOne(
|
||||
{ meetingId },
|
||||
{
|
||||
fields:
|
||||
{
|
||||
stopwatch: 1,
|
||||
time: 1,
|
||||
accumulated: 1,
|
||||
timestamp: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (timer) {
|
||||
const {
|
||||
timestamp,
|
||||
} = timer;
|
||||
|
||||
const accumulated = timer.accumulated + (now - timestamp);
|
||||
|
||||
const payload = {
|
||||
accumulated,
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
} else {
|
||||
Logger.warn(`Could not stop timer for meeting=${meetingId}, timer not found`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Stopping timer: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// This method should only be used by the server
|
||||
export function sysStopTimer(meetingId) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'StopTimerReqMsg';
|
||||
const USER_ID = 'nodeJSapp';
|
||||
|
||||
try {
|
||||
check(meetingId, String);
|
||||
const now = Date.now();
|
||||
const timer = Timer.findOne(
|
||||
{ meetingId },
|
||||
{
|
||||
fields:
|
||||
{
|
||||
stopwatch: 1,
|
||||
time: 1,
|
||||
accumulated: 1,
|
||||
timestamp: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (timer) {
|
||||
const {
|
||||
timestamp,
|
||||
} = timer;
|
||||
|
||||
const accumulated = timer.accumulated + (now - timestamp);
|
||||
|
||||
const payload = {
|
||||
accumulated,
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, USER_ID, payload);
|
||||
} else {
|
||||
Logger.warn(`Could not stop timer for meeting=${meetingId}, timer not found`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Stopping timer: ${err}`);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { check } from 'meteor/check';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
|
||||
export default function switchTimer(stopwatch) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'SwitchTimerReqMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(stopwatch, Boolean);
|
||||
|
||||
const payload = {
|
||||
stopwatch,
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
} catch (err) {
|
||||
Logger.error(`Switching timer: ${err}`);
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Timer from '/imports/api/timer';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { getInitialState } from '/imports/api/timer/server/helpers';
|
||||
|
||||
// This method should only be used by the server
|
||||
export default function addTimer(meetingId) {
|
||||
check(meetingId, String);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
meetingId,
|
||||
...getInitialState(),
|
||||
active: false,
|
||||
ended: 0,
|
||||
};
|
||||
|
||||
const cb = (err, numChanged) => {
|
||||
if (err) {
|
||||
return Logger.error(`Adding timer to the collection: ${err}`);
|
||||
}
|
||||
|
||||
if (numChanged) {
|
||||
return Logger.debug(`Added timer meeting=${meetingId}`);
|
||||
}
|
||||
|
||||
return Logger.debug(`Upserted timer meeting=${meetingId}`);
|
||||
};
|
||||
|
||||
return Timer.upsert(selector, modifier, cb);
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import Timer from '/imports/api/timer';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function clearTimer(meetingId) {
|
||||
if (meetingId) {
|
||||
return Timer.remove({ meetingId }, () => {
|
||||
Logger.info(`Cleared Timer (${meetingId})`);
|
||||
});
|
||||
}
|
||||
|
||||
return Timer.remove({}, () => {
|
||||
Logger.info('Cleared Timer (all)');
|
||||
});
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { check } from 'meteor/check';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function endTimer(meetingId) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'TimerEndedPubMsg';
|
||||
const USER_ID = 'nodeJSapp';
|
||||
|
||||
try {
|
||||
check(meetingId, String);
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, USER_ID, {});
|
||||
} catch (err) {
|
||||
Logger.error(`Ending timer: ${err}`);
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Timer from '/imports/api/timer';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function stopTimer(meetingId) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'StopTimerReqMsg';
|
||||
const USER_ID = 'nodeJSapp';
|
||||
|
||||
try {
|
||||
check(meetingId, String);
|
||||
const now = Date.now();
|
||||
const timer = Timer.findOne(
|
||||
{ meetingId },
|
||||
{
|
||||
fields:
|
||||
{
|
||||
stopwatch: 1,
|
||||
time: 1,
|
||||
accumulated: 1,
|
||||
timestamp: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (timer) {
|
||||
const {
|
||||
timestamp,
|
||||
} = timer;
|
||||
|
||||
const accumulated = timer.accumulated + (now - timestamp);
|
||||
|
||||
const payload = {
|
||||
accumulated,
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, USER_ID, payload);
|
||||
} else {
|
||||
Logger.warn(`Could not stop timer for meeting=${meetingId}, timer not found`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Stopping timer: ${err}`);
|
||||
}
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Timer from '/imports/api/timer';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import Users from '/imports/api/users';
|
||||
import { TRACKS, getInitialState } from '/imports/api/timer/server/helpers';
|
||||
import { sysStopTimer } from '../methods/stopTimer';
|
||||
import { sysEndTimer } from '../methods/endTimer';
|
||||
|
||||
const getActivateModifier = () => ({
|
||||
$set: {
|
||||
active: true,
|
||||
...getInitialState(),
|
||||
ended: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const getDeactivateModifier = () => ({
|
||||
$set: {
|
||||
active: false,
|
||||
running: false,
|
||||
ended: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const getResetModifier = () => ({
|
||||
$set: {
|
||||
accumulated: 0,
|
||||
timestamp: Date.now(),
|
||||
ended: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const handleTimerEndedNotifications = (fields, meetingId, handle) => {
|
||||
const meetingUsers = Users.find({ meetingId }).count();
|
||||
|
||||
if (fields.running === false) {
|
||||
handle.stop();
|
||||
}
|
||||
|
||||
if (fields.ended >= meetingUsers) {
|
||||
sysStopTimer(meetingId);
|
||||
sysEndTimer(meetingId);
|
||||
}
|
||||
};
|
||||
|
||||
const setTimerEndObserver = (meetingId) => {
|
||||
const { stopwatch } = Timer.findOne({ meetingId });
|
||||
|
||||
if (stopwatch === false) {
|
||||
const meetingTimer = Timer.find(
|
||||
{ meetingId },
|
||||
{ fields: { ended: 1, running: 1 } },
|
||||
);
|
||||
const handle = meetingTimer.observeChanges({
|
||||
changed: (id, fields) => {
|
||||
handleTimerEndedNotifications(fields, meetingId, handle);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getStartModifier = () => ({
|
||||
$set: {
|
||||
running: true,
|
||||
timestamp: Date.now(),
|
||||
ended: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const getStopModifier = (accumulated) => ({
|
||||
$set: {
|
||||
running: false,
|
||||
accumulated,
|
||||
timestamp: 0,
|
||||
ended: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const getSwitchModifier = (stopwatch) => ({
|
||||
$set: {
|
||||
stopwatch,
|
||||
running: false,
|
||||
accumulated: 0,
|
||||
timestamp: 0,
|
||||
track: TRACKS[0],
|
||||
ended: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const getSetModifier = (time) => ({
|
||||
$set: {
|
||||
running: false,
|
||||
accumulated: 0,
|
||||
timestamp: 0,
|
||||
time,
|
||||
},
|
||||
});
|
||||
|
||||
const getTrackModifier = (track) => ({
|
||||
$set: {
|
||||
track,
|
||||
},
|
||||
});
|
||||
|
||||
const getEndedModifier = () => ({
|
||||
$inc: {
|
||||
ended: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const logTimer = (meetingId, requesterUserId, action, stopwatch, time, track) => {
|
||||
if (action === 'switch') {
|
||||
Logger.info(`Timer: meetingId=${meetingId} requesterUserId=${requesterUserId} action=${action} stopwatch=${stopwatch} `);
|
||||
} else if (action === 'set' && time !== 0) {
|
||||
Logger.info(`Timer: meetingId=${meetingId} requesterUserId=${requesterUserId} action=${action} ${time}ms`);
|
||||
} else if (action === 'track') {
|
||||
Logger.info(`Timer: meetingId=${meetingId} requesterUserId=${requesterUserId} action=${action} changed to ${track}`);
|
||||
} else {
|
||||
Logger.info(`Timer: meetingId=${meetingId} requesterUserId=${requesterUserId} action=${action}`);
|
||||
}
|
||||
};
|
||||
|
||||
export default function updateTimer({
|
||||
action,
|
||||
meetingId,
|
||||
requesterUserId,
|
||||
time = 0,
|
||||
stopwatch = true,
|
||||
accumulated = 0,
|
||||
track = TRACKS[0],
|
||||
}) {
|
||||
check(action, String);
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(time, Number);
|
||||
check(stopwatch, Boolean);
|
||||
check(accumulated, Number);
|
||||
check(track, String);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
};
|
||||
|
||||
let modifier;
|
||||
|
||||
switch (action) {
|
||||
case 'activate':
|
||||
modifier = getActivateModifier();
|
||||
break;
|
||||
case 'deactivate':
|
||||
modifier = getDeactivateModifier();
|
||||
break;
|
||||
case 'reset':
|
||||
modifier = getResetModifier();
|
||||
break;
|
||||
case 'start':
|
||||
setTimerEndObserver(meetingId);
|
||||
modifier = getStartModifier();
|
||||
break;
|
||||
case 'stop':
|
||||
modifier = getStopModifier(accumulated);
|
||||
break;
|
||||
case 'switch':
|
||||
modifier = getSwitchModifier(stopwatch);
|
||||
break;
|
||||
case 'set':
|
||||
modifier = getSetModifier(time);
|
||||
break;
|
||||
case 'track':
|
||||
modifier = getTrackModifier(track);
|
||||
break;
|
||||
case 'ended':
|
||||
modifier = getEndedModifier();
|
||||
break;
|
||||
default:
|
||||
Logger.error(`Unhandled timer action=${action}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const { numberAffected } = Timer.upsert(selector, modifier);
|
||||
|
||||
if (numberAffected) {
|
||||
logTimer(meetingId, requesterUserId, action, stopwatch, time, track);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Updating timer: ${err}`);
|
||||
}
|
||||
}
|
22
bigbluebutton-html5/imports/api/timer/server/publishers.js
Normal file
22
bigbluebutton-html5/imports/api/timer/server/publishers.js
Normal file
@ -0,0 +1,22 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import Timer from '/imports/api/timer';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
|
||||
function timer() {
|
||||
if (!this.userId) {
|
||||
return Timer.find({ meetingId: '' });
|
||||
}
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
Logger.info(`Publishing timer for ${meetingId} ${requesterUserId}`);
|
||||
|
||||
return Timer.find({ meetingId });
|
||||
}
|
||||
|
||||
function publish(...args) {
|
||||
const boundTimer = timer.bind(this);
|
||||
return boundTimer(...args);
|
||||
}
|
||||
|
||||
Meteor.publish('timer', publish);
|
@ -7,10 +7,15 @@ import RandomUserSelectContainer from '/imports/ui/components/common/modal/rando
|
||||
import LayoutModalContainer from '/imports/ui/components/layout/modal/container';
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import Styled from './styles';
|
||||
import TimerService from '/imports/ui/components/timer/service';
|
||||
import { colorPrimary } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { PANELS, ACTIONS, LAYOUT_TYPE } from '../../layout/enums';
|
||||
import { uniqueId } from '/imports/utils/string-utils';
|
||||
import { isPresentationEnabled, isLayoutsEnabled } from '/imports/ui/services/features';
|
||||
import {
|
||||
isPresentationEnabled,
|
||||
isLayoutsEnabled,
|
||||
isTimerFeatureEnabled,
|
||||
} from '/imports/ui/services/features';
|
||||
import VideoPreviewContainer from '/imports/ui/components/video-preview/container';
|
||||
import { screenshareHasEnded } from '/imports/ui/components/screenshare/service';
|
||||
|
||||
@ -22,6 +27,8 @@ const propTypes = {
|
||||
amIModerator: PropTypes.bool.isRequired,
|
||||
shortcuts: PropTypes.string,
|
||||
handleTakePresenter: PropTypes.func.isRequired,
|
||||
isTimerActive: PropTypes.bool.isRequired,
|
||||
isTimerEnabled: PropTypes.bool.isRequired,
|
||||
allowExternalVideo: PropTypes.bool.isRequired,
|
||||
stopExternalVideoShare: PropTypes.func.isRequired,
|
||||
isMobile: PropTypes.bool.isRequired,
|
||||
@ -40,6 +47,14 @@ const intlMessages = defineMessages({
|
||||
id: 'app.actionsBar.actionsDropdown.actionsLabel',
|
||||
description: 'Actions button label',
|
||||
},
|
||||
activateTimerLabel: {
|
||||
id: 'app.actionsBar.actionsDropdown.activateTimerLabel',
|
||||
description: 'Activate timer label',
|
||||
},
|
||||
deactivateTimerLabel: {
|
||||
id: 'app.actionsBar.actionsDropdown.deactivateTimerLabel',
|
||||
description: 'Deactivate timer label',
|
||||
},
|
||||
presentationLabel: {
|
||||
id: 'app.actionsBar.actionsDropdown.presentationLabel',
|
||||
description: 'Upload a presentation option label',
|
||||
@ -115,6 +130,7 @@ class ActionsDropdown extends PureComponent {
|
||||
this.presentationItemId = uniqueId('action-item-');
|
||||
this.pollId = uniqueId('action-item-');
|
||||
this.takePresenterId = uniqueId('action-item-');
|
||||
this.timerId = uniqueId('action-item-');
|
||||
this.selectUserRandId = uniqueId('action-item-');
|
||||
this.state = {
|
||||
isExternalVideoModalOpen: false,
|
||||
@ -131,6 +147,7 @@ class ActionsDropdown extends PureComponent {
|
||||
this.setCameraAsContentModalIsOpen = this.setCameraAsContentModalIsOpen.bind(this);
|
||||
this.setPropsToPassModal = this.setPropsToPassModal.bind(this);
|
||||
this.setForceOpen = this.setForceOpen.bind(this);
|
||||
this.handleTimerClick = this.handleTimerClick.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@ -145,6 +162,15 @@ class ActionsDropdown extends PureComponent {
|
||||
this.setExternalVideoModalIsOpen(true);
|
||||
}
|
||||
|
||||
handleTimerClick() {
|
||||
const { isTimerActive } = this.props;
|
||||
if (!isTimerActive) {
|
||||
TimerService.activateTimer();
|
||||
} else {
|
||||
TimerService.deactivateTimer();
|
||||
}
|
||||
}
|
||||
|
||||
getAvailableActions() {
|
||||
const {
|
||||
intl,
|
||||
@ -155,6 +181,8 @@ class ActionsDropdown extends PureComponent {
|
||||
isPollingEnabled,
|
||||
isSelectRandomUserEnabled,
|
||||
stopExternalVideoShare,
|
||||
isTimerActive,
|
||||
isTimerEnabled,
|
||||
layoutContextDispatch,
|
||||
setMeetingLayout,
|
||||
setPushLayout,
|
||||
@ -241,6 +269,16 @@ class ActionsDropdown extends PureComponent {
|
||||
})
|
||||
}
|
||||
|
||||
if (amIModerator && isTimerEnabled && isTimerFeatureEnabled()) {
|
||||
actions.push({
|
||||
icon: 'time',
|
||||
label: isTimerActive ? intl.formatMessage(intlMessages.deactivateTimerLabel)
|
||||
: intl.formatMessage(intlMessages.activateTimerLabel),
|
||||
key: this.timerId,
|
||||
onClick: () => this.handleTimerClick(),
|
||||
});
|
||||
}
|
||||
|
||||
if (amIPresenter && showPushLayout && isLayoutsEnabled()) {
|
||||
actions.push({
|
||||
icon: 'send',
|
||||
|
@ -54,6 +54,8 @@ class ActionsBar extends PureComponent {
|
||||
hasGenericContent,
|
||||
hasCameraAsContent,
|
||||
stopExternalVideoShare,
|
||||
isTimerActive,
|
||||
isTimerEnabled,
|
||||
isCaptionsAvailable,
|
||||
isMeteorConnected,
|
||||
isPollingEnabled,
|
||||
@ -61,8 +63,6 @@ class ActionsBar extends PureComponent {
|
||||
isRaiseHandButtonCentered,
|
||||
isThereCurrentPresentation,
|
||||
allowExternalVideo,
|
||||
setEmojiStatus,
|
||||
currentUser,
|
||||
layoutContextDispatch,
|
||||
actionsBarStyle,
|
||||
setMeetingLayout,
|
||||
@ -93,6 +93,8 @@ class ActionsBar extends PureComponent {
|
||||
intl,
|
||||
isSharingVideo,
|
||||
stopExternalVideoShare,
|
||||
isTimerActive,
|
||||
isTimerEnabled,
|
||||
isMeteorConnected,
|
||||
setMeetingLayout,
|
||||
setPushLayout,
|
||||
|
@ -12,6 +12,7 @@ import Service from './service';
|
||||
import UserListService from '/imports/ui/components/user-list/service';
|
||||
import ExternalVideoService from '/imports/ui/components/external-video-player/service';
|
||||
import CaptionsService from '/imports/ui/components/captions/service';
|
||||
import TimerService from '/imports/ui/components/timer/service';
|
||||
import { layoutSelectOutput, layoutDispatch } from '../layout/context';
|
||||
import { isExternalVideoEnabled, isPollingEnabled, isPresentationEnabled } from '/imports/ui/services/features';
|
||||
import { isScreenBroadcasting, isCameraAsContentBroadcasting } from '/imports/ui/components/screenshare/service';
|
||||
@ -67,6 +68,8 @@ export default withTracker(() => ({
|
||||
hasScreenshare: isScreenBroadcasting(),
|
||||
hasCameraAsContent: isCameraAsContentBroadcasting(),
|
||||
isCaptionsAvailable: CaptionsService.isCaptionsAvailable(),
|
||||
isTimerActive: TimerService.isActive(),
|
||||
isTimerEnabled: TimerService.isEnabled(),
|
||||
isMeteorConnected: Meteor.status().connected,
|
||||
isPollingEnabled: isPollingEnabled() && isPresentationEnabled(),
|
||||
isSelectRandomUserEnabled: SELECT_RANDOM_USER_ENABLED,
|
||||
|
@ -49,6 +49,7 @@ import AudioService from '/imports/ui/components/audio/service';
|
||||
import NotesContainer from '/imports/ui/components/notes/container';
|
||||
import DEFAULT_VALUES from '../layout/defaultValues';
|
||||
import AppService from '/imports/ui/components/app/service';
|
||||
import TimerService from '/imports/ui/components/timer/service';
|
||||
|
||||
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
@ -135,6 +136,9 @@ class App extends Component {
|
||||
isVideoPreviewModalOpen: false,
|
||||
};
|
||||
|
||||
this.isTimerEnabled = TimerService.isEnabled();
|
||||
this.timeOffsetInterval = null;
|
||||
|
||||
this.handleWindowResize = throttle(this.handleWindowResize).bind(this);
|
||||
this.shouldAriaHide = this.shouldAriaHide.bind(this);
|
||||
this.setAudioModalIsOpen = this.setAudioModalIsOpen.bind(this);
|
||||
@ -206,6 +210,12 @@ class App extends Component {
|
||||
|
||||
ConnectionStatusService.startRoundTripTime();
|
||||
|
||||
if (this.isTimerEnabled) {
|
||||
TimerService.fetchTimeOffset();
|
||||
this.timeOffsetInterval = setInterval(TimerService.fetchTimeOffset,
|
||||
TimerService.OFFSET_INTERVAL);
|
||||
}
|
||||
|
||||
logger.info({ logCode: 'app_component_componentdidmount' }, 'Client loaded successfully');
|
||||
}
|
||||
|
||||
@ -284,6 +294,10 @@ class App extends Component {
|
||||
window.removeEventListener('resize', this.handleWindowResize, false);
|
||||
window.onbeforeunload = null;
|
||||
ConnectionStatusService.stopRoundTripTime();
|
||||
|
||||
if (this.timeOffsetInterval) {
|
||||
clearInterval(this.timeOffsetInterval);
|
||||
}
|
||||
}
|
||||
|
||||
handleWindowResize() {
|
||||
|
@ -106,6 +106,7 @@ export const PANELS = {
|
||||
CAPTIONS: 'captions',
|
||||
BREAKOUT: 'breakoutroom',
|
||||
SHARED_NOTES: 'shared-notes',
|
||||
TIMER: 'timer',
|
||||
WAITING_USERS: 'waiting-users',
|
||||
NONE: 'none',
|
||||
};
|
||||
|
@ -9,6 +9,7 @@ import ConnectionStatusButton from '/imports/ui/components/connection-status/but
|
||||
import ConnectionStatusService from '/imports/ui/components/connection-status/service';
|
||||
import { addNewAlert } from '/imports/ui/components/screenreader-alert/service';
|
||||
import SettingsDropdownContainer from './settings-dropdown/container';
|
||||
import TimerIndicatorContainer from '/imports/ui/components/timer/indicator/container';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import { PANELS, ACTIONS } from '../layout/enums';
|
||||
@ -249,6 +250,7 @@ class NavBar extends Component {
|
||||
</Styled.Top>
|
||||
<Styled.Bottom>
|
||||
<TalkingIndicatorContainer amIModerator={amIModerator} />
|
||||
<TimerIndicatorContainer />
|
||||
</Styled.Bottom>
|
||||
</Styled.Navbar>
|
||||
);
|
||||
@ -258,4 +260,3 @@ class NavBar extends Component {
|
||||
NavBar.propTypes = propTypes;
|
||||
NavBar.defaultProps = defaultProps;
|
||||
export default withShortcutHelper(injectIntl(NavBar), 'toggleUserList');
|
||||
|
||||
|
@ -7,6 +7,7 @@ import NotesContainer from '/imports/ui/components/notes/container';
|
||||
import PollContainer from '/imports/ui/components/poll/container';
|
||||
import CaptionsContainer from '/imports/ui/components/captions/container';
|
||||
import BreakoutRoomContainer from '/imports/ui/components/breakout-room/container';
|
||||
import TimerContainer from '/imports/ui/components/timer/container';
|
||||
import WaitingUsersPanel from '/imports/ui/components/waiting-users/container';
|
||||
import Styled from './styles';
|
||||
import ErrorBoundary from '/imports/ui/components/common/error-boundary/component';
|
||||
@ -145,6 +146,7 @@ const SidebarContent = (props) => {
|
||||
)}
|
||||
{sidebarContentPanel === PANELS.CAPTIONS && <CaptionsContainer />}
|
||||
{sidebarContentPanel === PANELS.BREAKOUT && <BreakoutRoomContainer />}
|
||||
{sidebarContentPanel === PANELS.TIMER && <TimerContainer />}
|
||||
{sidebarContentPanel === PANELS.WAITING_USERS && <WaitingUsersPanel />}
|
||||
{sidebarContentPanel === PANELS.POLL && (
|
||||
<Styled.Poll style={{ minWidth, top: '0', display: pollDisplay }} id="pollPanel">
|
||||
|
@ -23,7 +23,7 @@ const SUBSCRIPTIONS = [
|
||||
'local-settings', 'users-typing', 'record-meetings', 'video-streams',
|
||||
'connection-status', 'voice-call-states', 'external-video-meetings', 'breakouts', 'breakouts-history',
|
||||
'pads', 'pads-sessions', 'pads-updates', 'notifications', 'audio-captions',
|
||||
'layout-meetings', 'user-reaction',
|
||||
'layout-meetings', 'user-reaction', 'timer',
|
||||
];
|
||||
const {
|
||||
localBreakoutsSync,
|
||||
|
446
bigbluebutton-html5/imports/ui/components/timer/component.jsx
Normal file
446
bigbluebutton-html5/imports/ui/components/timer/component.jsx
Normal file
@ -0,0 +1,446 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Service from './service';
|
||||
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
|
||||
import Styled from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
hideTimerLabel: {
|
||||
id: 'app.timer.hideTimerLabel',
|
||||
description: 'Label for hiding timer button',
|
||||
},
|
||||
title: {
|
||||
id: 'app.timer.title',
|
||||
description: 'Title for timer',
|
||||
},
|
||||
stopwatch: {
|
||||
id: 'app.timer.button.stopwatch',
|
||||
description: 'Stopwatch switch button',
|
||||
},
|
||||
timer: {
|
||||
id: 'app.timer.button.timer',
|
||||
description: 'Timer switch button',
|
||||
},
|
||||
start: {
|
||||
id: 'app.timer.button.start',
|
||||
description: 'Timer start button',
|
||||
},
|
||||
stop: {
|
||||
id: 'app.timer.button.stop',
|
||||
description: 'Timer stop button',
|
||||
},
|
||||
reset: {
|
||||
id: 'app.timer.button.reset',
|
||||
description: 'Timer reset button',
|
||||
},
|
||||
hours: {
|
||||
id: 'app.timer.hours',
|
||||
description: 'Timer hours label',
|
||||
},
|
||||
minutes: {
|
||||
id: 'app.timer.minutes',
|
||||
description: 'Timer minutes label',
|
||||
},
|
||||
seconds: {
|
||||
id: 'app.timer.seconds',
|
||||
description: 'Timer seconds label',
|
||||
},
|
||||
songs: {
|
||||
id: 'app.timer.songs',
|
||||
description: 'Songs title label',
|
||||
},
|
||||
noTrack: {
|
||||
id: 'app.timer.noTrack',
|
||||
description: 'No track radio label',
|
||||
},
|
||||
track1: {
|
||||
id: 'app.timer.track1',
|
||||
description: 'Track 1 radio label',
|
||||
},
|
||||
track2: {
|
||||
id: 'app.timer.track2',
|
||||
description: 'Track 2 radio label',
|
||||
},
|
||||
track3: {
|
||||
id: 'app.timer.track3',
|
||||
description: 'Track 3 radio label',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
timer: PropTypes.shape({
|
||||
stopwatch: PropTypes.bool,
|
||||
running: PropTypes.bool,
|
||||
time: PropTypes.string,
|
||||
accumulated: PropTypes.number,
|
||||
timestamp: PropTypes.number,
|
||||
}).isRequired,
|
||||
layoutContextDispatch: PropTypes.shape().isRequired,
|
||||
timeOffset: PropTypes.number.isRequired,
|
||||
isRTL: PropTypes.bool.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
isModerator: PropTypes.bool.isRequired,
|
||||
currentTrack: PropTypes.string.isRequired,
|
||||
isResizing: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
class Timer extends Component {
|
||||
static handleOnTrackChange(event) {
|
||||
Service.setTrack(event.target.value);
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.timeRef = React.createRef();
|
||||
this.interval = null;
|
||||
|
||||
this.updateTime = this.updateTime.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { timer } = this.props;
|
||||
const { running } = timer;
|
||||
|
||||
const { current } = this.timeRef;
|
||||
if (current && running) {
|
||||
this.interval = setInterval(this.updateTime, Service.getInterval());
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { timer } = this.props;
|
||||
const { timer: prevTimer } = prevProps;
|
||||
|
||||
this.updateInterval(prevTimer, timer);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
handleControlClick() {
|
||||
const { timer } = this.props;
|
||||
|
||||
if (timer.running) {
|
||||
Service.stopTimer();
|
||||
} else {
|
||||
Service.startTimer();
|
||||
}
|
||||
}
|
||||
|
||||
handleOnHoursChange(event) {
|
||||
const { timer } = this.props;
|
||||
const { target } = event;
|
||||
|
||||
if (target && target.value) {
|
||||
const hours = parseInt(target.value, 10);
|
||||
Service.setHours(hours, timer.time);
|
||||
}
|
||||
}
|
||||
|
||||
handleOnMinutesChange(event) {
|
||||
const { timer } = this.props;
|
||||
const { target } = event;
|
||||
|
||||
if (target && target.value) {
|
||||
const minutes = parseInt(target.value, 10);
|
||||
Service.setMinutes(minutes, timer.time);
|
||||
}
|
||||
}
|
||||
|
||||
handleOnSecondsChange(event) {
|
||||
const { timer } = this.props;
|
||||
const { target } = event;
|
||||
|
||||
if (target && target.value) {
|
||||
const seconds = parseInt(target.value, 10);
|
||||
Service.setSeconds(seconds, timer.time);
|
||||
}
|
||||
}
|
||||
|
||||
handleSwitchToStopwatch() {
|
||||
const { timer } = this.props;
|
||||
|
||||
if (!timer.stopwatch) {
|
||||
Service.switchTimer(true);
|
||||
}
|
||||
}
|
||||
|
||||
handleSwitchToTimer() {
|
||||
const { timer } = this.props;
|
||||
|
||||
if (timer.stopwatch) {
|
||||
Service.switchTimer(false);
|
||||
}
|
||||
}
|
||||
|
||||
getTime() {
|
||||
const {
|
||||
timer,
|
||||
timeOffset,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
stopwatch,
|
||||
running,
|
||||
time,
|
||||
accumulated,
|
||||
timestamp,
|
||||
} = timer;
|
||||
|
||||
const elapsedTime = Service.getElapsedTime(running, timestamp, timeOffset, accumulated);
|
||||
|
||||
let updatedTime;
|
||||
if (stopwatch) {
|
||||
updatedTime = elapsedTime;
|
||||
} else {
|
||||
updatedTime = Math.max(time - elapsedTime, 0);
|
||||
}
|
||||
|
||||
return Service.getTimeAsString(updatedTime, stopwatch);
|
||||
}
|
||||
|
||||
updateTime() {
|
||||
const { current } = this.timeRef;
|
||||
if (current) {
|
||||
current.textContent = this.getTime();
|
||||
}
|
||||
}
|
||||
|
||||
updateInterval(prevTimer, timer) {
|
||||
const { running } = timer;
|
||||
const { running: prevRunning } = prevTimer;
|
||||
|
||||
if (!prevRunning && running) {
|
||||
this.interval = setInterval(this.updateTime, Service.getInterval());
|
||||
}
|
||||
|
||||
if (prevRunning && !running) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
renderControls() {
|
||||
const {
|
||||
intl,
|
||||
timer,
|
||||
} = this.props;
|
||||
|
||||
const { running } = timer;
|
||||
|
||||
const label = running ? intlMessages.stop : intlMessages.start;
|
||||
const color = running ? 'danger' : 'primary';
|
||||
|
||||
return (
|
||||
<Styled.TimerControls>
|
||||
<Styled.TimerControlButton
|
||||
color={color}
|
||||
label={intl.formatMessage(label)}
|
||||
onClick={() => this.handleControlClick()}
|
||||
/>
|
||||
<Styled.TimerControlButton
|
||||
label={intl.formatMessage(intlMessages.reset)}
|
||||
onClick={() => Service.resetTimer()}
|
||||
/>
|
||||
</Styled.TimerControls>
|
||||
);
|
||||
}
|
||||
|
||||
renderSongSelectorRadios() {
|
||||
const {
|
||||
intl,
|
||||
timer,
|
||||
currentTrack,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
stopwatch,
|
||||
} = timer;
|
||||
|
||||
return (
|
||||
<Styled.TimerSongsWrapper>
|
||||
<Styled.TimerSongsTitle
|
||||
stopwatch={stopwatch}
|
||||
>
|
||||
{intl.formatMessage(intlMessages.songs)}
|
||||
</Styled.TimerSongsTitle>
|
||||
<Styled.TimerTracks>
|
||||
{Service.TRACKS.map((track) => (
|
||||
<Styled.TimerTrackItem
|
||||
key={track}
|
||||
>
|
||||
<label htmlFor={track}>
|
||||
<input
|
||||
type="radio"
|
||||
name="track"
|
||||
id={track}
|
||||
value={track}
|
||||
checked={currentTrack === track}
|
||||
onChange={Timer.handleOnTrackChange}
|
||||
disabled={stopwatch}
|
||||
/>
|
||||
{intl.formatMessage(intlMessages[track])}
|
||||
</label>
|
||||
</Styled.TimerTrackItem>
|
||||
))}
|
||||
</Styled.TimerTracks>
|
||||
</Styled.TimerSongsWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
renderTimer() {
|
||||
const {
|
||||
intl,
|
||||
timer,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
time,
|
||||
stopwatch,
|
||||
} = timer;
|
||||
|
||||
const timeArray = Service.getTimeAsString(time).split(':');
|
||||
|
||||
const hasHours = timeArray.length === 3;
|
||||
|
||||
const hours = hasHours ? timeArray[0] : '00';
|
||||
const minutes = hasHours ? timeArray[1] : timeArray[0];
|
||||
const seconds = hasHours ? timeArray[2] : timeArray[1];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Styled.StopwatchTime>
|
||||
<Styled.StopwatchTimeInput>
|
||||
<input
|
||||
type="number"
|
||||
disabled={stopwatch}
|
||||
value={hours}
|
||||
maxLength="2"
|
||||
max={Service.getMaxHours()}
|
||||
min="0"
|
||||
onChange={(event) => this.handleOnHoursChange(event)}
|
||||
/>
|
||||
<Styled.StopwatchTimeInputLabel>
|
||||
{intl.formatMessage(intlMessages.hours)}
|
||||
</Styled.StopwatchTimeInputLabel>
|
||||
</Styled.StopwatchTimeInput>
|
||||
<Styled.StopwatchTimeColon>:</Styled.StopwatchTimeColon>
|
||||
<Styled.StopwatchTimeInput>
|
||||
<input
|
||||
type="number"
|
||||
disabled={stopwatch}
|
||||
value={minutes}
|
||||
maxLength="2"
|
||||
max="59"
|
||||
min="0"
|
||||
onChange={(event) => this.handleOnMinutesChange(event)}
|
||||
/>
|
||||
<Styled.StopwatchTimeInputLabel>
|
||||
{intl.formatMessage(intlMessages.minutes)}
|
||||
</Styled.StopwatchTimeInputLabel>
|
||||
</Styled.StopwatchTimeInput>
|
||||
<Styled.StopwatchTimeColon>:</Styled.StopwatchTimeColon>
|
||||
<Styled.StopwatchTimeInput>
|
||||
<input
|
||||
type="number"
|
||||
disabled={stopwatch}
|
||||
value={seconds}
|
||||
maxLength="2"
|
||||
max="59"
|
||||
min="0"
|
||||
onChange={(event) => this.handleOnSecondsChange(event)}
|
||||
/>
|
||||
<Styled.StopwatchTimeInputLabel>
|
||||
{intl.formatMessage(intlMessages.seconds)}
|
||||
</Styled.StopwatchTimeInputLabel>
|
||||
</Styled.StopwatchTimeInput>
|
||||
</Styled.StopwatchTime>
|
||||
{ Service.isMusicEnabled()
|
||||
? this.renderSongSelectorRadios() : null}
|
||||
{this.renderControls()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const {
|
||||
intl,
|
||||
isResizing,
|
||||
timer,
|
||||
} = this.props;
|
||||
|
||||
const { stopwatch } = timer;
|
||||
|
||||
return (
|
||||
<Styled.TimerContent
|
||||
isResizing={isResizing}
|
||||
>
|
||||
<Styled.TimerCurrent
|
||||
aria-hidden
|
||||
ref={this.timeRef}
|
||||
>
|
||||
{this.getTime()}
|
||||
</Styled.TimerCurrent>
|
||||
<Styled.TimerType>
|
||||
<Styled.TimerSwitchButton
|
||||
label={intl.formatMessage(intlMessages.stopwatch)}
|
||||
onClick={() => this.handleSwitchToStopwatch()}
|
||||
color={stopwatch ? 'primary' : 'default'}
|
||||
/>
|
||||
<Styled.TimerSwitchButton
|
||||
label={intl.formatMessage(intlMessages.timer)}
|
||||
onClick={() => this.handleSwitchToTimer()}
|
||||
color={!stopwatch ? 'primary' : 'default'}
|
||||
/>
|
||||
</Styled.TimerType>
|
||||
{this.renderTimer()}
|
||||
</Styled.TimerContent>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
isRTL,
|
||||
isActive,
|
||||
isModerator,
|
||||
layoutContextDispatch,
|
||||
timer,
|
||||
} = this.props;
|
||||
|
||||
if (!isActive || !isModerator) {
|
||||
Service.closePanel(layoutContextDispatch);
|
||||
return null;
|
||||
}
|
||||
|
||||
const { stopwatch } = timer;
|
||||
const message = stopwatch ? intlMessages.stopwatch : intlMessages.timer;
|
||||
|
||||
return (
|
||||
<Styled.TimerSidebarContent
|
||||
data-test="timer"
|
||||
>
|
||||
<Styled.TimerHeader>
|
||||
<Styled.TimerTitle>
|
||||
<Styled.TimerMinimizeButton
|
||||
onClick={() => Service.closePanel(layoutContextDispatch)}
|
||||
aria-label={intl.formatMessage(intlMessages.hideTimerLabel)}
|
||||
label={intl.formatMessage(message)}
|
||||
icon={isRTL ? 'right_arrow' : 'left_arrow'}
|
||||
/>
|
||||
</Styled.TimerTitle>
|
||||
</Styled.TimerHeader>
|
||||
{this.renderContent()}
|
||||
</Styled.TimerSidebarContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Timer.propTypes = propTypes;
|
||||
|
||||
export default injectWbResizeEvent(injectIntl(Timer));
|
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Timer from './component';
|
||||
import Service from './service';
|
||||
import { layoutSelectInput, layoutDispatch } from '/imports/ui/components/layout/context';
|
||||
|
||||
const TimerContainer = ({ children, ...props }) => {
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
const cameraDock = layoutSelectInput((i) => i.cameraDock);
|
||||
const { isResizing } = cameraDock;
|
||||
return (
|
||||
<Timer {...{ layoutContextDispatch, isResizing, ...props }}>
|
||||
{children}
|
||||
</Timer>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTracker(() => {
|
||||
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
|
||||
return {
|
||||
isRTL,
|
||||
isActive: Service.isActive(),
|
||||
isModerator: Service.isModerator(),
|
||||
timeOffset: Service.getTimeOffset(),
|
||||
timer: Service.getTimer(),
|
||||
currentTrack: Service.getCurrentTrack(),
|
||||
};
|
||||
})(TimerContainer);
|
@ -0,0 +1,402 @@
|
||||
import React, { Component } from 'react';
|
||||
import Icon from '/imports/ui/components/common/icon/component';
|
||||
import TimerService from '/imports/ui/components/timer/service';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import PropTypes from 'prop-types';
|
||||
import Styled from './styles';
|
||||
|
||||
const CDN = Meteor.settings.public.app.cdn;
|
||||
const BASENAME = Meteor.settings.public.app.basename;
|
||||
const HOST = CDN + BASENAME;
|
||||
const trackName = Meteor.settings.public.timer.music;
|
||||
const TAB_TIMER_INDICATOR = Meteor.settings.public.timer.tabIndicator;
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
timer: PropTypes.shape({
|
||||
stopwatch: PropTypes.bool,
|
||||
running: PropTypes.bool,
|
||||
time: PropTypes.string,
|
||||
accumulated: PropTypes.number,
|
||||
timestamp: PropTypes.number,
|
||||
}).isRequired,
|
||||
isTimerActive: PropTypes.bool.isRequired,
|
||||
isMusicActive: PropTypes.bool.isRequired,
|
||||
sidebarNavigationIsOpen: PropTypes.bool.isRequired,
|
||||
sidebarContentIsOpen: PropTypes.bool.isRequired,
|
||||
timeOffset: PropTypes.number.isRequired,
|
||||
isModerator: PropTypes.bool.isRequired,
|
||||
currentTrack: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
class Indicator extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.timeRef = React.createRef();
|
||||
this.interval = null;
|
||||
|
||||
this.alarm = null;
|
||||
this.music = null;
|
||||
|
||||
// We need to avoid trigger on mount
|
||||
this.triggered = true;
|
||||
|
||||
this.alreadyNotified = false;
|
||||
|
||||
this.updateTime = this.updateTime.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { timer } = this.props;
|
||||
const { running } = timer;
|
||||
|
||||
this.alarm = new Audio(`${HOST}/resources/sounds/alarm.mp3`);
|
||||
this.setUpMusic();
|
||||
|
||||
this.triggered = this.initTriggered();
|
||||
|
||||
const { current } = this.timeRef;
|
||||
if (current && running) {
|
||||
this.interval = setInterval(this.updateTime, TimerService.getInterval());
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { timer, isTimerActive } = this.props;
|
||||
const { timer: prevTimer, isTimerActive: prevTimerActive } = prevProps;
|
||||
|
||||
if (this.shouldPlayMusic()) {
|
||||
this.playMusic();
|
||||
}
|
||||
|
||||
if (!isTimerActive && prevTimerActive) {
|
||||
this.updateTabTitleTimer(true, this.getTime());
|
||||
}
|
||||
|
||||
this.updateInterval(prevTimer, timer);
|
||||
this.updateAlarmTrigger(prevTimer, timer);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
this.stopMusic();
|
||||
}
|
||||
|
||||
getTime() {
|
||||
const {
|
||||
timer,
|
||||
timeOffset,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
stopwatch,
|
||||
running,
|
||||
time,
|
||||
accumulated,
|
||||
timestamp,
|
||||
} = timer;
|
||||
|
||||
const elapsedTime = TimerService.getElapsedTime(running, timestamp, timeOffset, accumulated);
|
||||
|
||||
let updatedTime;
|
||||
if (stopwatch) {
|
||||
updatedTime = elapsedTime;
|
||||
} else {
|
||||
updatedTime = Math.max(time - elapsedTime, 0);
|
||||
}
|
||||
|
||||
if (this.shouldNotifyTimerEnded(updatedTime)) {
|
||||
TimerService.timerEnded();
|
||||
}
|
||||
|
||||
if (this.shouldStopMusic(updatedTime)) {
|
||||
this.stopMusic();
|
||||
}
|
||||
|
||||
if (this.soundAlarm(updatedTime)) {
|
||||
this.play();
|
||||
}
|
||||
|
||||
return TimerService.getTimeAsString(updatedTime, stopwatch);
|
||||
}
|
||||
|
||||
setUpMusic() {
|
||||
const { currentTrack } = this.props;
|
||||
if (trackName[currentTrack] === undefined) return;
|
||||
if (this.music === null) {
|
||||
this.music = new Audio(`${HOST}/resources/sounds/${trackName[currentTrack]}.mp3`);
|
||||
this.music.volume = TimerService.getMusicVolume();
|
||||
this.music.addEventListener('timeupdate', () => {
|
||||
const buffer = 0.19;
|
||||
// Start playing the music before it ends to make the loop gapless
|
||||
if (this.music.currentTime > this.music.duration - buffer) {
|
||||
this.music.currentTime = 0;
|
||||
this.music.play();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.music.src = `${HOST}/resources/sounds/${trackName[currentTrack]}.mp3`;
|
||||
}
|
||||
this.music.track = currentTrack;
|
||||
}
|
||||
|
||||
updateInterval(prevTimer, timer) {
|
||||
const { running } = timer;
|
||||
const { running: prevRunning } = prevTimer;
|
||||
|
||||
if (!prevRunning && running) {
|
||||
this.interval = setInterval(this.updateTime, TimerService.getInterval());
|
||||
}
|
||||
|
||||
if (prevRunning && !running) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
updateAlarmTrigger(prevTimer, timer) {
|
||||
const {
|
||||
accumulated,
|
||||
timestamp,
|
||||
} = timer;
|
||||
|
||||
const { timestamp: prevTimestamp } = prevTimer;
|
||||
|
||||
const reseted = timestamp !== prevTimestamp && accumulated === 0;
|
||||
|
||||
if (reseted) {
|
||||
this.triggered = false;
|
||||
this.alreadyNotified = false;
|
||||
}
|
||||
}
|
||||
|
||||
initTriggered() {
|
||||
const {
|
||||
timer,
|
||||
timeOffset,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
stopwatch,
|
||||
running,
|
||||
} = timer;
|
||||
|
||||
if (stopwatch || !running) return false;
|
||||
|
||||
const {
|
||||
time,
|
||||
accumulated,
|
||||
timestamp,
|
||||
} = timer;
|
||||
|
||||
const elapsedTime = TimerService.getElapsedTime(running, timestamp, timeOffset, accumulated);
|
||||
const updatedTime = Math.max(time - elapsedTime, 0);
|
||||
|
||||
if (updatedTime === 0) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
play() {
|
||||
if (this.alarm && !this.triggered) {
|
||||
this.triggered = true;
|
||||
this.alarm.play().catch((error) => {
|
||||
logger.error({
|
||||
logCode: 'timer_sound_error',
|
||||
extraInfo: { error },
|
||||
}, `Timer beep failed: ${error}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
soundAlarm(time) {
|
||||
const { timer } = this.props;
|
||||
const {
|
||||
running,
|
||||
stopwatch,
|
||||
} = timer;
|
||||
|
||||
const enabled = TimerService.isAlarmEnabled();
|
||||
const zero = time === 0;
|
||||
|
||||
return enabled && running && zero && !stopwatch;
|
||||
}
|
||||
|
||||
playMusic() {
|
||||
const handleUserInteraction = () => {
|
||||
this.music.play();
|
||||
window.removeEventListener('click', handleUserInteraction);
|
||||
window.removeEventListener('auxclick', handleUserInteraction);
|
||||
window.removeEventListener('keydown', handleUserInteraction);
|
||||
window.removeEventListener('touchstart', handleUserInteraction);
|
||||
};
|
||||
|
||||
const playMusicAfterUserInteraction = () => {
|
||||
window.addEventListener('click', handleUserInteraction);
|
||||
window.addEventListener('auxclick', handleUserInteraction);
|
||||
window.addEventListener('keydown', handleUserInteraction);
|
||||
window.addEventListener('touchstart', handleUserInteraction);
|
||||
};
|
||||
|
||||
this.handleUserInteraction = handleUserInteraction;
|
||||
|
||||
if (this.music !== null) {
|
||||
this.music.play()
|
||||
.catch((error) => {
|
||||
if (error.name === 'NotAllowedError') {
|
||||
playMusicAfterUserInteraction();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
stopMusic() {
|
||||
if (this.music !== null) {
|
||||
this.music.pause();
|
||||
this.music.currentTime = 0;
|
||||
window.removeEventListener('click', this.handleUserInteraction);
|
||||
window.removeEventListener('auxclick', this.handleUserInteraction);
|
||||
window.removeEventListener('keydown', this.handleUserInteraction);
|
||||
window.removeEventListener('touchstart', this.handleUserInteraction);
|
||||
}
|
||||
}
|
||||
|
||||
shouldPlayMusic() {
|
||||
const {
|
||||
timer,
|
||||
isMusicActive,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
running,
|
||||
stopwatch,
|
||||
} = timer;
|
||||
const validMusic = this.music != null;
|
||||
if (!validMusic) return false;
|
||||
|
||||
const musicIsPlaying = !this.music.paused;
|
||||
|
||||
return !musicIsPlaying && isMusicActive && running && !stopwatch;
|
||||
}
|
||||
|
||||
shouldStopMusic(time) {
|
||||
const {
|
||||
timer,
|
||||
isTimerActive,
|
||||
isMusicActive,
|
||||
currentTrack,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
running,
|
||||
stopwatch,
|
||||
} = timer;
|
||||
|
||||
const zero = time === 0;
|
||||
const validMusic = this.music != null;
|
||||
const musicIsPlaying = validMusic && !this.music.paused;
|
||||
const trackChanged = this.music?.track !== currentTrack;
|
||||
|
||||
const reachedZeroOrStopped = (running && zero) || (!running);
|
||||
|
||||
return musicIsPlaying
|
||||
&& (!isMusicActive || stopwatch || reachedZeroOrStopped || !isTimerActive || trackChanged);
|
||||
}
|
||||
|
||||
shouldNotifyTimerEnded(time) {
|
||||
const { timer } = this.props;
|
||||
const {
|
||||
running,
|
||||
stopwatch,
|
||||
} = timer;
|
||||
|
||||
if (stopwatch || !running) return false;
|
||||
|
||||
const reachedZero = time === 0;
|
||||
|
||||
if (reachedZero && !this.alreadyNotified) {
|
||||
this.alreadyNotified = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
updateTime() {
|
||||
const { current } = this.timeRef;
|
||||
if (current) {
|
||||
current.textContent = this.getTime();
|
||||
this.updateTabTitleTimer(false, current.textContent);
|
||||
}
|
||||
}
|
||||
|
||||
updateTabTitleTimer(deactivation, timeString) {
|
||||
if (!TAB_TIMER_INDICATOR) return;
|
||||
|
||||
const matchTimerString = /\[[0-9][0-9]:[0-9][0-9]:[0-9][0-9]\]/g;
|
||||
|
||||
if (deactivation) {
|
||||
document.title = document.title.replace(matchTimerString, '');
|
||||
} else {
|
||||
if (RegExp(matchTimerString).test(document.title)) {
|
||||
document.title = document.title.replace(matchTimerString, '');
|
||||
document.title = '[' + timeString + '] ' + document.title;
|
||||
} else {
|
||||
document.title = '[' + timeString + '] ' + document.title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isTimerActive } = this.props;
|
||||
const time = this.getTime();
|
||||
if (!isTimerActive) return null;
|
||||
|
||||
const {
|
||||
isModerator,
|
||||
timer,
|
||||
sidebarNavigationIsOpen,
|
||||
sidebarContentIsOpen,
|
||||
currentTrack,
|
||||
} = this.props;
|
||||
const { running } = timer;
|
||||
const trackChanged = this.music?.track !== currentTrack;
|
||||
if (trackChanged) {
|
||||
this.setUpMusic();
|
||||
}
|
||||
|
||||
const onClick = running ? TimerService.stopTimer : TimerService.startTimer;
|
||||
this.updateTabTitleTimer(false, time);
|
||||
return (
|
||||
<Styled.TimerWrapper>
|
||||
<Styled.Timer>
|
||||
<Styled.TimerButton
|
||||
running={running}
|
||||
disabled={!isModerator}
|
||||
hide={sidebarNavigationIsOpen && sidebarContentIsOpen}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={isModerator ? onClick : null}
|
||||
>
|
||||
<Styled.TimerContent>
|
||||
<Styled.TimerIcon>
|
||||
<Icon iconName="time" />
|
||||
</Styled.TimerIcon>
|
||||
<Styled.TimerTime
|
||||
aria-hidden
|
||||
ref={this.timeRef}
|
||||
>
|
||||
{time}
|
||||
</Styled.TimerTime>
|
||||
</Styled.TimerContent>
|
||||
</Styled.TimerButton>
|
||||
</Styled.Timer>
|
||||
</Styled.TimerWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Indicator.propTypes = propTypes;
|
||||
|
||||
export default Indicator;
|
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Indicator from './component';
|
||||
import TimerService from '/imports/ui/components/timer/service';
|
||||
import { layoutSelectInput } from '/imports/ui/components/layout/context';
|
||||
|
||||
const IndicatorContainer = (props) => {
|
||||
const sidebarNavigation = layoutSelectInput((i) => i.sidebarNavigation);
|
||||
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
|
||||
const sidebarNavigationIsOpen = sidebarNavigation.isOpen;
|
||||
const sidebarContentIsOpen = sidebarContent.isOpen;
|
||||
|
||||
return (
|
||||
<Indicator
|
||||
{...{
|
||||
sidebarNavigationIsOpen,
|
||||
sidebarContentIsOpen,
|
||||
...props,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTracker(() => ({
|
||||
timer: TimerService.getTimer(),
|
||||
timeOffset: TimerService.getTimeOffset(),
|
||||
isModerator: TimerService.isModerator(),
|
||||
isTimerActive: TimerService.isActive(),
|
||||
isMusicActive: TimerService.isMusicActive(),
|
||||
currentTrack: TimerService.getCurrentTrack(),
|
||||
}))(IndicatorContainer);
|
@ -0,0 +1,131 @@
|
||||
import styled from 'styled-components';
|
||||
import { phoneLandscape, smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
|
||||
import { borderRadius } from '/imports/ui/stylesheets/styled-components/general';
|
||||
import {
|
||||
colorSuccess,
|
||||
colorDanger,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { fontSizeBase, fontSizeXS } from '/imports/ui/stylesheets/styled-components/typography';
|
||||
|
||||
const colorTimerRunning = `${colorSuccess}`;
|
||||
const colorTimerStopped = `${colorDanger}`;
|
||||
const timerMarginSM = '.5rem';
|
||||
const timerPaddingSM = '.25rem';
|
||||
const timerPaddingXL = '1.62rem';
|
||||
const timerMaxWidth = '10rem';
|
||||
const timerFontWeight = '400';
|
||||
const timerBorderRadius = '2rem';
|
||||
|
||||
const TimerWrapper = styled.div`
|
||||
overflow: hidden;
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
const Timer = styled.div`
|
||||
display: flex;
|
||||
max-height: ${timerPaddingXL});
|
||||
`;
|
||||
|
||||
const timerRunning = `
|
||||
background-color: ${colorTimerRunning};
|
||||
border: solid 2px ${colorTimerRunning};
|
||||
`;
|
||||
|
||||
const timerStopped = `
|
||||
background-color: ${colorTimerStopped};
|
||||
border: solid 2px ${colorTimerStopped};
|
||||
`;
|
||||
|
||||
const disabledStyle = `
|
||||
cursor: default;
|
||||
`;
|
||||
|
||||
const hiddenStyle = `
|
||||
@media ${smallOnly} {
|
||||
visibility: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
const TimerButton = styled.div`
|
||||
@include highContrastOutline();
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
font-weight: ${timerFontWeight};
|
||||
border-radius: ${timerBorderRadius} ${timerBorderRadius};
|
||||
font-size: ${fontSizeBase};
|
||||
margin-left: ${borderRadius};
|
||||
margin-right: ${borderRadius};
|
||||
|
||||
@media ${phoneLandscape} {
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: ${timerMaxWidth};
|
||||
|
||||
@media ${phoneLandscape} {
|
||||
font-size: ${fontSizeXS};
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: var(--font-size-small);
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
|
||||
@media ${phoneLandscape} {
|
||||
height: ${timerMarginSM};
|
||||
width: ${timerMarginSM};
|
||||
font-size: ${fontSizeXS};
|
||||
}
|
||||
}
|
||||
|
||||
${({ running }) => (running ? timerRunning : timerStopped)};
|
||||
${({ disabled }) => disabled && disabledStyle};
|
||||
${({ hide }) => hide && hiddenStyle};
|
||||
`;
|
||||
|
||||
const time = `
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-self: center;
|
||||
padding: 0 ${timerPaddingSM} 0 0;
|
||||
`;
|
||||
|
||||
const TimerContent = styled.div`
|
||||
${time}
|
||||
display: flex;
|
||||
|
||||
[dir="ltr"] & {
|
||||
span:first-child {
|
||||
padding: 0 ${timerPaddingSM};
|
||||
}
|
||||
}
|
||||
|
||||
[dir="rtl"] & {
|
||||
span:last-child {
|
||||
padding: 0 ${timerPaddingSM};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const TimerIcon = styled.span`
|
||||
${time}
|
||||
`;
|
||||
|
||||
const TimerTime = styled.span`
|
||||
${time}
|
||||
`;
|
||||
|
||||
export default {
|
||||
TimerWrapper,
|
||||
Timer,
|
||||
TimerButton,
|
||||
TimerContent,
|
||||
TimerIcon,
|
||||
TimerTime,
|
||||
};
|
330
bigbluebutton-html5/imports/ui/components/timer/service.js
Normal file
330
bigbluebutton-html5/imports/ui/components/timer/service.js
Normal file
@ -0,0 +1,330 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import Timer from '/imports/api/timer';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import { Session } from 'meteor/session';
|
||||
import Users from '/imports/api/users';
|
||||
import Logger from '/imports/startup/client/logger';
|
||||
import { ACTIONS, PANELS } from '../layout/enums';
|
||||
|
||||
const TIMER_CONFIG = Meteor.settings.public.timer;
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
const OFFSET_INTERVAL = TIMER_CONFIG.interval.offset;
|
||||
|
||||
const MILLI_IN_HOUR = 3600000;
|
||||
const MILLI_IN_MINUTE = 60000;
|
||||
const MILLI_IN_SECOND = 1000;
|
||||
|
||||
const MAX_HOURS = 23;
|
||||
|
||||
const TRACKS = [
|
||||
'noTrack',
|
||||
'track1',
|
||||
'track2',
|
||||
'track3',
|
||||
];
|
||||
|
||||
const isMusicEnabled = () => TIMER_CONFIG.music.enabled;
|
||||
|
||||
const getCurrentTrack = () => {
|
||||
const timer = Timer.findOne(
|
||||
{ meetingId: Auth.meetingID },
|
||||
{ fields: { track: 1 } },
|
||||
);
|
||||
|
||||
if (timer) return isMusicEnabled() && timer.track;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const isEnabled = () => TIMER_CONFIG.enabled;
|
||||
|
||||
const getMaxHours = () => MAX_HOURS;
|
||||
|
||||
const isAlarmEnabled = () => isEnabled() && TIMER_CONFIG.alarm;
|
||||
|
||||
const isMusicActive = () => getCurrentTrack() !== TRACKS[0];
|
||||
|
||||
const getMusicVolume = () => TIMER_CONFIG.music.volume;
|
||||
|
||||
const getMusicTrack = () => TIMER_CONFIG.music.track;
|
||||
|
||||
const isActive = () => {
|
||||
const timer = Timer.findOne(
|
||||
{ meetingId: Auth.meetingID },
|
||||
{ fields: { active: 1 } },
|
||||
);
|
||||
|
||||
if (timer) return timer.active;
|
||||
return false;
|
||||
};
|
||||
|
||||
const getDefaultTime = () => TIMER_CONFIG.time * MILLI_IN_MINUTE;
|
||||
|
||||
const getInterval = () => TIMER_CONFIG.interval.clock;
|
||||
|
||||
const isRunning = () => {
|
||||
const timer = Timer.findOne(
|
||||
{ meetingId: Auth.meetingID },
|
||||
{ fields: { running: 1 } },
|
||||
);
|
||||
|
||||
if (timer) return timer.running;
|
||||
return false;
|
||||
};
|
||||
|
||||
const isStopwatch = () => {
|
||||
const timer = Timer.findOne(
|
||||
{ meetingId: Auth.meetingID },
|
||||
{ fields: { stopwatch: 1 } },
|
||||
);
|
||||
|
||||
if (timer) return timer.stopwatch;
|
||||
return false;
|
||||
};
|
||||
|
||||
const startTimer = () => makeCall('startTimer');
|
||||
|
||||
const stopTimer = () => makeCall('stopTimer');
|
||||
|
||||
const switchTimer = (stopwatch) => makeCall('switchTimer', stopwatch);
|
||||
|
||||
const setTimer = (time) => makeCall('setTimer', time);
|
||||
|
||||
const resetTimer = () => makeCall('resetTimer');
|
||||
|
||||
const activateTimer = () => makeCall('activateTimer');
|
||||
|
||||
const deactivateTimer = () => makeCall('deactivateTimer');
|
||||
|
||||
const timerEnded = () => makeCall('timerEnded');
|
||||
|
||||
const setTrack = (track) => {
|
||||
makeCall('setTrack', track);
|
||||
};
|
||||
|
||||
const fetchTimeOffset = () => {
|
||||
const t0 = Date.now();
|
||||
|
||||
makeCall('getServerTime').then((result) => {
|
||||
if (result === 0) return;
|
||||
const t3 = Date.now();
|
||||
|
||||
const ts = result;
|
||||
const rtt = t3 - t0;
|
||||
const timeOffset = Math.round(ts - rtt / 2 - t0);
|
||||
|
||||
Session.set('timeOffset', timeOffset);
|
||||
});
|
||||
};
|
||||
|
||||
const getTimeOffset = () => {
|
||||
const timeOffset = Session.get('timeOffset');
|
||||
|
||||
if (timeOffset) return timeOffset;
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const getElapsedTime = (running, timestamp, timeOffset, accumulated) => {
|
||||
if (!running) return accumulated;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
return accumulated + Math.abs(now - timestamp + timeOffset);
|
||||
};
|
||||
|
||||
const getStopwatch = () => {
|
||||
const timer = Timer.findOne(
|
||||
{ meetingId: Auth.meetingID },
|
||||
{ fields: { stopwatch: 1 } },
|
||||
);
|
||||
|
||||
if (timer) return timer.stopwatch;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const getTimer = () => {
|
||||
const timer = Timer.findOne(
|
||||
{ meetingId: Auth.meetingID },
|
||||
{
|
||||
fields:
|
||||
{
|
||||
stopwatch: 1,
|
||||
running: 1,
|
||||
time: 1,
|
||||
accumulated: 1,
|
||||
timestamp: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (timer) {
|
||||
const {
|
||||
stopwatch,
|
||||
running,
|
||||
time,
|
||||
accumulated,
|
||||
timestamp,
|
||||
} = timer;
|
||||
|
||||
return {
|
||||
stopwatch,
|
||||
running,
|
||||
time,
|
||||
accumulated,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
stopwatch: true,
|
||||
running: false,
|
||||
time: getDefaultTime(),
|
||||
accumulated: 0,
|
||||
timestamp: 0,
|
||||
};
|
||||
};
|
||||
|
||||
const getTimeAsString = (time) => {
|
||||
const milliseconds = time;
|
||||
|
||||
const hours = Math.floor(milliseconds / MILLI_IN_HOUR);
|
||||
const mHours = hours * MILLI_IN_HOUR;
|
||||
|
||||
const minutes = Math.floor((milliseconds - mHours) / MILLI_IN_MINUTE);
|
||||
const mMinutes = minutes * MILLI_IN_MINUTE;
|
||||
|
||||
const seconds = Math.floor((milliseconds - mHours - mMinutes) / MILLI_IN_SECOND);
|
||||
|
||||
let timeAsString = '';
|
||||
|
||||
if (hours < 10) {
|
||||
timeAsString += `0${hours}:`;
|
||||
} else {
|
||||
timeAsString += `${hours}:`;
|
||||
}
|
||||
|
||||
if (minutes < 10) {
|
||||
timeAsString += `0${minutes}:`;
|
||||
} else {
|
||||
timeAsString += `${minutes}:`;
|
||||
}
|
||||
|
||||
if (seconds < 10) {
|
||||
timeAsString += `0${seconds}`;
|
||||
} else {
|
||||
timeAsString += `${seconds}`;
|
||||
}
|
||||
|
||||
return timeAsString;
|
||||
};
|
||||
|
||||
const closePanel = (layoutContextDispatch) => {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.NONE,
|
||||
});
|
||||
};
|
||||
|
||||
const togglePanel = (sidebarContentPanel, layoutContextDispatch) => {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: sidebarContentPanel !== PANELS.TIMER,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: sidebarContentPanel === PANELS.TIMER
|
||||
? PANELS.NONE
|
||||
: PANELS.TIMER,
|
||||
});
|
||||
};
|
||||
|
||||
const isModerator = () => Users.findOne(
|
||||
{ userId: Auth.userID },
|
||||
{ fields: { role: 1 } },
|
||||
).role === ROLE_MODERATOR;
|
||||
|
||||
const setHours = (hours, time) => {
|
||||
if (!Number.isNaN(hours) && hours >= 0 && hours <= MAX_HOURS) {
|
||||
const currentHours = Math.floor(time / MILLI_IN_HOUR);
|
||||
|
||||
const diff = (hours - currentHours) * MILLI_IN_HOUR;
|
||||
setTimer(time + diff);
|
||||
} else {
|
||||
Logger.warn('Invalid time');
|
||||
}
|
||||
};
|
||||
|
||||
const setMinutes = (minutes, time) => {
|
||||
if (!Number.isNaN(minutes) && minutes >= 0 && minutes <= 59) {
|
||||
const currentHours = Math.floor(time / MILLI_IN_HOUR);
|
||||
const mHours = currentHours * MILLI_IN_HOUR;
|
||||
|
||||
const currentMinutes = Math.floor((time - mHours) / MILLI_IN_MINUTE);
|
||||
|
||||
const diff = (minutes - currentMinutes) * MILLI_IN_MINUTE;
|
||||
setTimer(time + diff);
|
||||
} else {
|
||||
Logger.warn('Invalid time');
|
||||
}
|
||||
};
|
||||
|
||||
const setSeconds = (seconds, time) => {
|
||||
if (!Number.isNaN(seconds) && seconds >= 0 && seconds <= 59) {
|
||||
const currentHours = Math.floor(time / MILLI_IN_HOUR);
|
||||
const mHours = currentHours * MILLI_IN_HOUR;
|
||||
|
||||
const currentMinutes = Math.floor((time - mHours) / MILLI_IN_MINUTE);
|
||||
const mMinutes = currentMinutes * MILLI_IN_MINUTE;
|
||||
|
||||
const currentSeconds = Math.floor((time - mHours - mMinutes) / MILLI_IN_SECOND);
|
||||
|
||||
const diff = (seconds - currentSeconds) * MILLI_IN_SECOND;
|
||||
setTimer(time + diff);
|
||||
} else {
|
||||
Logger.warn('Invalid time');
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
OFFSET_INTERVAL,
|
||||
TRACKS,
|
||||
isActive,
|
||||
isEnabled,
|
||||
isMusicEnabled,
|
||||
isMusicActive,
|
||||
getCurrentTrack,
|
||||
getMusicVolume,
|
||||
getMusicTrack,
|
||||
isRunning,
|
||||
isStopwatch,
|
||||
isAlarmEnabled,
|
||||
startTimer,
|
||||
stopTimer,
|
||||
switchTimer,
|
||||
setHours,
|
||||
setMinutes,
|
||||
setSeconds,
|
||||
resetTimer,
|
||||
activateTimer,
|
||||
deactivateTimer,
|
||||
fetchTimeOffset,
|
||||
setTrack,
|
||||
getTimeOffset,
|
||||
getElapsedTime,
|
||||
getInterval,
|
||||
getMaxHours,
|
||||
getStopwatch,
|
||||
getTimer,
|
||||
getTimeAsString,
|
||||
closePanel,
|
||||
togglePanel,
|
||||
isModerator,
|
||||
timerEnded,
|
||||
};
|
229
bigbluebutton-html5/imports/ui/components/timer/styles.js
Normal file
229
bigbluebutton-html5/imports/ui/components/timer/styles.js
Normal file
@ -0,0 +1,229 @@
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
borderSize,
|
||||
borderSizeLarge,
|
||||
mdPaddingX,
|
||||
mdPaddingY,
|
||||
pollHeaderOffset,
|
||||
toastContentWidth,
|
||||
} from '../../stylesheets/styled-components/general';
|
||||
import { colorGrayDark, colorGrayLightest, colorWhite } from '../../stylesheets/styled-components/palette';
|
||||
import { TextElipsis } from '../../stylesheets/styled-components/placeholders';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
|
||||
const TimerSidebarContent = styled.div`
|
||||
background-color: ${colorWhite};
|
||||
padding:
|
||||
${mdPaddingX}
|
||||
${mdPaddingY}
|
||||
${mdPaddingX}
|
||||
${mdPaddingX};
|
||||
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
transform: translateZ(0);
|
||||
`;
|
||||
|
||||
const TimerHeader = styled.header`
|
||||
position: relative;
|
||||
top: ${pollHeaderOffset};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const TimerTitle = styled.div`
|
||||
${TextElipsis};
|
||||
flex: 1;
|
||||
|
||||
& > button, button:hover {
|
||||
max-width: ${toastContentWidth};
|
||||
}
|
||||
`;
|
||||
|
||||
const TimerMinimizeButton = styled(Button)`
|
||||
position: relative;
|
||||
background-color: ${colorWhite};
|
||||
display: block;
|
||||
margin: ${borderSizeLarge};
|
||||
margin-bottom: ${borderSize};
|
||||
padding-left: 0;
|
||||
padding-right: inherit;
|
||||
|
||||
[dir="rtl"] & {
|
||||
padding-left: inherit;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
> i {
|
||||
color: ${colorGrayDark};
|
||||
font-size: smaller;
|
||||
|
||||
[dir="rtl"] & {
|
||||
-webkit-transform: scale(-1, 1);
|
||||
-moz-transform: scale(-1, 1);
|
||||
-ms-transform: scale(-1, 1);
|
||||
-o-transform: scale(-1, 1);
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: ${colorWhite};
|
||||
}
|
||||
`;
|
||||
|
||||
const TimerContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const TimerCurrent = styled.span`
|
||||
border-bottom: 1px solid ${colorGrayLightest};
|
||||
border-top: 1px solid ${colorGrayLightest};
|
||||
display: flex;
|
||||
font-size: xxx-large;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const TimerType = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
padding-top: 2rem;
|
||||
`;
|
||||
|
||||
const TimerSwitchButton = styled(Button)`
|
||||
width: 100%;
|
||||
height: 2rem;
|
||||
margin: 0 .5rem;
|
||||
`;
|
||||
|
||||
const StopwatchTime = styled.div`
|
||||
display: flex;
|
||||
margin-top: 4rem;
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
font-size: x-large;
|
||||
justify-content: center;
|
||||
|
||||
input {
|
||||
width: 5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const StopwatchTimeInput = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
font-size: small;
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const StopwatchTimeInputLabel = styled.div`
|
||||
display: flex;
|
||||
font-size: small;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StopwatchTimeColon = styled.span`
|
||||
align-self: center;
|
||||
padding: 0 .25rem;
|
||||
`;
|
||||
|
||||
const TimerSongsWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-flow: column;
|
||||
margin-top: 4rem;
|
||||
margin-bottom: -2rem;
|
||||
`;
|
||||
|
||||
const TimerRow = `
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
const TimerCol = `
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
`;
|
||||
const TimerSongsTitle = styled.div`
|
||||
${TimerRow}
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
opacity: ${({ stopwatch }) => (stopwatch ? '50%' : '100%')}
|
||||
`;
|
||||
|
||||
const TimerTracks = styled.div`
|
||||
${TimerCol}
|
||||
display: flex;
|
||||
margin-top: 0.8rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.row {
|
||||
margin: 0.5rem auto;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
input {
|
||||
margin: auto 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const TimerTrackItem = styled.div`
|
||||
${TimerRow}
|
||||
`;
|
||||
|
||||
const TimerControls = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin-top: 4rem;
|
||||
height: 3rem;
|
||||
`;
|
||||
|
||||
const TimerControlButton = styled(Button)`
|
||||
width: 6rem;
|
||||
margin: 0 1rem;
|
||||
`;
|
||||
|
||||
export default {
|
||||
TimerSidebarContent,
|
||||
TimerHeader,
|
||||
TimerTitle,
|
||||
TimerMinimizeButton,
|
||||
TimerContent,
|
||||
TimerCurrent,
|
||||
TimerType,
|
||||
TimerSwitchButton,
|
||||
StopwatchTime,
|
||||
StopwatchTimeInput,
|
||||
StopwatchTimeInputLabel,
|
||||
StopwatchTimeColon,
|
||||
TimerSongsWrapper,
|
||||
TimerSongsTitle,
|
||||
TimerTracks,
|
||||
TimerTrackItem,
|
||||
TimerControls,
|
||||
TimerControlButton,
|
||||
};
|
@ -4,6 +4,7 @@ import Styled from './styles';
|
||||
import UserParticipantsContainer from './user-participants/container';
|
||||
import UserMessagesContainer from './user-messages/container';
|
||||
import UserNotesContainer from './user-notes/container';
|
||||
import TimerContainer from './timer/container';
|
||||
import UserCaptionsContainer from './user-captions/container';
|
||||
import WaitingUsersContainer from './waiting-users/container';
|
||||
import UserPollsContainer from './user-polls/container';
|
||||
@ -12,6 +13,7 @@ import { isChatEnabled } from '/imports/ui/services/features';
|
||||
|
||||
const propTypes = {
|
||||
currentUser: PropTypes.shape({}).isRequired,
|
||||
isTimerActive: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
@ -21,6 +23,7 @@ class UserContent extends PureComponent {
|
||||
render() {
|
||||
const {
|
||||
currentUser,
|
||||
isTimerActive,
|
||||
pendingUsers,
|
||||
isWaitingRoomEnabled,
|
||||
isGuestLobbyMessageEnabled,
|
||||
@ -35,6 +38,11 @@ class UserContent extends PureComponent {
|
||||
{isChatEnabled() ? <UserMessagesContainer /> : null}
|
||||
{currentUser.role === ROLE_MODERATOR ? <UserCaptionsContainer /> : null}
|
||||
<UserNotesContainer />
|
||||
{ isTimerActive && (
|
||||
<TimerContainer
|
||||
isModerator={currentUser?.role === ROLE_MODERATOR}
|
||||
/>
|
||||
) }
|
||||
{showWaitingRoom && currentUser.role === ROLE_MODERATOR
|
||||
? (
|
||||
<WaitingUsersContainer {...{ pendingUsers }} />
|
||||
|
@ -3,6 +3,7 @@ import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import UserContent from './component';
|
||||
import GuestUsers from '/imports/api/guest-users';
|
||||
import TimerService from '/imports/ui/components/timer/service';
|
||||
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
|
||||
import WaitingUsersService from '/imports/ui/components/waiting-users/service';
|
||||
|
||||
@ -29,6 +30,7 @@ const UserContentContainer = (props) => {
|
||||
};
|
||||
|
||||
export default withTracker(() => ({
|
||||
isTimerActive: TimerService.isActive(),
|
||||
pendingUsers: GuestUsers.find({
|
||||
meetingId: Auth.meetingID,
|
||||
approved: false,
|
||||
|
@ -112,7 +112,7 @@ const ListItem = styled(Styled.ListItem)`
|
||||
flex-grow: 1;
|
||||
line-height: 2;
|
||||
text-align: left;
|
||||
padding-left: ${lgPaddingY};
|
||||
padding-left: 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
@ -0,0 +1,88 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Icon from '/imports/ui/components/common/icon/component';
|
||||
import TimerService from '/imports/ui/components/timer/service';
|
||||
import { PANELS, ACTIONS } from '../../../layout/enums';
|
||||
import Styled from './styles';
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
stopwatch: PropTypes.bool.isRequired,
|
||||
sidebarContentPanel: PropTypes.shape().isRequired,
|
||||
layoutContextDispatch: PropTypes.shape().isRequired,
|
||||
isModerator: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
title: {
|
||||
id: 'app.userList.timerTitle',
|
||||
description: 'Title for the time',
|
||||
},
|
||||
timer: {
|
||||
id: 'app.timer.timer.title',
|
||||
description: 'Title for the timer',
|
||||
},
|
||||
stopwatch: {
|
||||
id: 'app.timer.stopwatch.title',
|
||||
description: 'Title for the stopwatch',
|
||||
},
|
||||
});
|
||||
|
||||
class Timer extends PureComponent {
|
||||
componentDidMount() {
|
||||
const { layoutContextDispatch } = this.props;
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.TIMER,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
isModerator,
|
||||
stopwatch,
|
||||
sidebarContentPanel,
|
||||
layoutContextDispatch,
|
||||
} = this.props;
|
||||
|
||||
if (!isModerator) return null;
|
||||
|
||||
const message = stopwatch ? intlMessages.stopwatch : intlMessages.timer;
|
||||
|
||||
return (
|
||||
<Styled.Messages>
|
||||
<Styled.Container>
|
||||
<Styled.SmallTitle>
|
||||
{intl.formatMessage(intlMessages.title)}
|
||||
</Styled.SmallTitle>
|
||||
</Styled.Container>
|
||||
<Styled.ScrollableList>
|
||||
<Styled.List>
|
||||
<Styled.ListItem
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => TimerService.togglePanel(sidebarContentPanel, layoutContextDispatch)}
|
||||
>
|
||||
<Icon iconName="time" />
|
||||
<span>
|
||||
{intl.formatMessage(message)}
|
||||
</span>
|
||||
</Styled.ListItem>
|
||||
</Styled.List>
|
||||
</Styled.ScrollableList>
|
||||
</Styled.Messages>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Timer.propTypes = propTypes;
|
||||
|
||||
export default injectIntl(Timer);
|
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import TimerService from '/imports/ui/components/timer/service';
|
||||
import Timer from './component';
|
||||
import { layoutSelectInput, layoutDispatch } from '../../../layout/context';
|
||||
|
||||
const TimerContainer = (props) => {
|
||||
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
|
||||
const { sidebarContentPanel } = sidebarContent;
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
return <Timer {...{ layoutContextDispatch, sidebarContentPanel, ...props }} />;
|
||||
};
|
||||
|
||||
export default withTracker(() => ({
|
||||
isModerator: TimerService.isModerator(),
|
||||
stopwatch: TimerService.getStopwatch(),
|
||||
}))(TimerContainer);
|
@ -0,0 +1,27 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Styled from '/imports/ui/components/user-list/styles';
|
||||
import StyledContent from '/imports/ui/components/user-list/user-list-content/styles';
|
||||
|
||||
const ListItem = styled(StyledContent.ListItem)`
|
||||
i{ left: 4px; }
|
||||
`;
|
||||
|
||||
const Messages = styled(Styled.Messages)``;
|
||||
|
||||
const Container = styled(StyledContent.Container)``;
|
||||
|
||||
const SmallTitle = styled(Styled.SmallTitle)``;
|
||||
|
||||
const ScrollableList = styled(StyledContent.ScrollableList)``;
|
||||
|
||||
const List = styled(StyledContent.List)``;
|
||||
|
||||
export default {
|
||||
ListItem,
|
||||
Messages,
|
||||
Container,
|
||||
SmallTitle,
|
||||
ScrollableList,
|
||||
List,
|
||||
};
|
@ -17,7 +17,9 @@ const MessagesTitle = styled(Styled.SmallTitle)`
|
||||
|
||||
const ScrollableList = styled(StyledContent.ScrollableList)``;
|
||||
|
||||
const List = styled(StyledContent.List)``;
|
||||
const List = styled(StyledContent.List)`
|
||||
background-color: var(--color-off-white,#F3F6F9);
|
||||
`;
|
||||
|
||||
const ListTransition = styled.div`
|
||||
display: flex;
|
||||
|
@ -14,10 +14,7 @@ const UnreadMessages = styled(StyledContent.UnreadMessages)``;
|
||||
const UnreadMessagesText = styled(StyledContent.UnreadMessagesText)``;
|
||||
|
||||
const ListItem = styled(StyledContent.ListItem)`
|
||||
${({ $disabled }) => $disabled && `
|
||||
cursor: not-allowed;
|
||||
border: none;
|
||||
`}
|
||||
i{ left: 4px; }
|
||||
`;
|
||||
|
||||
const NotesTitle = styled.div`
|
||||
|
@ -83,3 +83,7 @@ export function isPresentationEnabled() {
|
||||
export function isReactionsEnabled() {
|
||||
return getDisabledFeatures().indexOf('reactions') === -1;
|
||||
}
|
||||
|
||||
export function isTimerFeatureEnabled() {
|
||||
return getDisabledFeatures().indexOf('timer') === -1;
|
||||
}
|
||||
|
@ -529,6 +529,20 @@ public:
|
||||
size: 24px
|
||||
lines: 2
|
||||
time: 5000
|
||||
timer:
|
||||
enabled: false
|
||||
alarm: true
|
||||
music:
|
||||
enabled: false
|
||||
volume: 0.4
|
||||
track1: "RelaxingMusic"
|
||||
track2: "CalmMusic"
|
||||
track3: "aristocratDrums"
|
||||
interval:
|
||||
clock: 100
|
||||
offset: 60000
|
||||
time: 5
|
||||
tabIndicator: false
|
||||
chat:
|
||||
enabled: true
|
||||
itemsPerPage: 100
|
||||
|
@ -54,6 +54,23 @@
|
||||
"app.emojiPicker.skintones.4": "Medium Skin Tone",
|
||||
"app.emojiPicker.skintones.5": "Medium-Dark Skin Tone",
|
||||
"app.emojiPicker.skintones.6": "Dark Skin Tone",
|
||||
"app.timer.title": "Time",
|
||||
"app.timer.stopwatch.title": "Stopwatch",
|
||||
"app.timer.timer.title": "Timer",
|
||||
"app.timer.hideTimerLabel": "Hide time",
|
||||
"app.timer.button.stopwatch": "Stopwatch",
|
||||
"app.timer.button.timer": "Timer",
|
||||
"app.timer.button.start": "Start",
|
||||
"app.timer.button.stop": "Stop",
|
||||
"app.timer.button.reset": "Reset",
|
||||
"app.timer.hours": "hours",
|
||||
"app.timer.minutes": "minutes",
|
||||
"app.timer.seconds": "seconds",
|
||||
"app.timer.songs": "Songs",
|
||||
"app.timer.noTrack": "No song",
|
||||
"app.timer.track1": "Relaxing",
|
||||
"app.timer.track2": "Calm",
|
||||
"app.timer.track3": "Happy",
|
||||
"app.captions.label": "Captions",
|
||||
"app.captions.menu.close": "Close",
|
||||
"app.captions.menu.start": "Start",
|
||||
@ -105,6 +122,7 @@
|
||||
"app.userList.messagesTitle": "Messages",
|
||||
"app.userList.notesTitle": "Notes",
|
||||
"app.userList.notesListItem.unreadContent": "New content is available in the shared notes section",
|
||||
"app.userList.timerTitle": "Time",
|
||||
"app.userList.captionsTitle": "Captions",
|
||||
"app.userList.presenter": "Presenter",
|
||||
"app.userList.you": "You",
|
||||
@ -590,6 +608,8 @@
|
||||
"app.talkingIndicator.moreThanMaxIndicatorsWereTalking" : "{0}+ were talking",
|
||||
"app.talkingIndicator.wasTalking" : "{0} stopped talking",
|
||||
"app.actionsBar.actionsDropdown.actionsLabel": "Actions",
|
||||
"app.actionsBar.actionsDropdown.activateTimerLabel": "Activate stopwatch",
|
||||
"app.actionsBar.actionsDropdown.deactivateTimerLabel": "Deactivate stopwatch",
|
||||
"app.actionsBar.actionsDropdown.presentationLabel": "Upload/Manage presentations",
|
||||
"app.actionsBar.actionsDropdown.initPollLabel": "Initiate a poll",
|
||||
"app.actionsBar.actionsDropdown.desktopShareLabel": "Share your screen",
|
||||
|
BIN
bigbluebutton-html5/public/resources/sounds/CalmMusic.mp3
Normal file
BIN
bigbluebutton-html5/public/resources/sounds/CalmMusic.mp3
Normal file
Binary file not shown.
BIN
bigbluebutton-html5/public/resources/sounds/RelaxingMusic.mp3
Normal file
BIN
bigbluebutton-html5/public/resources/sounds/RelaxingMusic.mp3
Normal file
Binary file not shown.
BIN
bigbluebutton-html5/public/resources/sounds/alarm.mp3
Normal file
BIN
bigbluebutton-html5/public/resources/sounds/alarm.mp3
Normal file
Binary file not shown.
BIN
bigbluebutton-html5/public/resources/sounds/aristocratDrums.mp3
Normal file
BIN
bigbluebutton-html5/public/resources/sounds/aristocratDrums.mp3
Normal file
Binary file not shown.
@ -23,6 +23,7 @@ import '/imports/api/video-streams/server';
|
||||
import '/imports/api/users-infos/server';
|
||||
import '/imports/api/users-persistent-data/server';
|
||||
import '/imports/api/connection-status/server';
|
||||
import '/imports/api/timer/server';
|
||||
import '/imports/api/audio-captions/server';
|
||||
import '/imports/api/external-videos/server';
|
||||
import '/imports/api/pads/server';
|
||||
|
Loading…
Reference in New Issue
Block a user