Merge pull request #18133 from GuiLeme/port-timer-with-events-suggestions

feat(timer): Port timer (Mconf) with events suggestions
This commit is contained in:
Anton Georgiev 2023-06-08 15:56:10 -04:00 committed by GitHub
commit c8b0437df3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 3722 additions and 13 deletions

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}
}

View File

@ -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()
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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()
}
}
}

View File

@ -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)
}
}
}

View File

@ -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");
}
}
}
}

View File

@ -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)
}

View File

@ -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()
}
}

View File

@ -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

View File

@ -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")
}

View File

@ -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"
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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")
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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")
}

View File

@ -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,

View File

@ -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)

View File

@ -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(

View File

@ -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)

View File

@ -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)

View File

@ -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);
}

View File

@ -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),

View 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;

View File

@ -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);

View File

@ -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,
});
}

View File

@ -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,
});
}

View File

@ -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',
});
}

View File

@ -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,
});
}

View File

@ -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,
});
}

View File

@ -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,
});
}

View File

@ -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,
});
}

View File

@ -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,
});
}

View File

@ -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,
});
}

View 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,
};

View File

@ -0,0 +1,3 @@
import './methods';
import './eventHandlers';
import './publishers';

View 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,
});

View File

@ -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}`);
}
}

View File

@ -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}`);
}
}

View File

@ -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}`);
}
}

View File

@ -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}`);
}
}

View File

@ -0,0 +1,5 @@
export default function getServerTime() {
if (this.userId) return Date.now();
return 0;
}

View File

@ -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}`);
}
}

View File

@ -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}`);
}
}

View File

@ -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}`);
}
}

View File

@ -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}`);
}
}

View File

@ -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}`);
}
}

View File

@ -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}`);
}
}

View File

@ -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);
}

View File

@ -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)');
});
}

View File

@ -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}`);
}
}

View File

@ -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}`);
}
}

View File

@ -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}`);
}
}

View 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);

View File

@ -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',

View File

@ -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,

View File

@ -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,

View File

@ -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() {

View File

@ -106,6 +106,7 @@ export const PANELS = {
CAPTIONS: 'captions',
BREAKOUT: 'breakoutroom',
SHARED_NOTES: 'shared-notes',
TIMER: 'timer',
WAITING_USERS: 'waiting-users',
NONE: 'none',
};

View File

@ -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');

View File

@ -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">

View File

@ -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,

View 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));

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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,
};

View 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,
};

View 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,
};

View File

@ -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 }} />

View File

@ -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,

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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,
};

View File

@ -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;

View File

@ -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`

View File

@ -83,3 +83,7 @@ export function isPresentationEnabled() {
export function isReactionsEnabled() {
return getDisabledFeatures().indexOf('reactions') === -1;
}
export function isTimerFeatureEnabled() {
return getDisabledFeatures().indexOf('timer') === -1;
}

View File

@ -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

View File

@ -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",

Binary file not shown.

View File

@ -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';