Merge remote-tracking branch 'upstream/v3.0.x-release' into i-19517
This commit is contained in:
commit
516e2a7260
@ -56,7 +56,7 @@ object ClientSettings extends SystemConfiguration {
|
||||
getConfigPropertyValueByPath(map, path) match {
|
||||
case Some(configValue: Int) => configValue
|
||||
case _ =>
|
||||
logger.debug("Config `{}` not found.", path)
|
||||
logger.debug(s"Config `$path` with type Integer not found in clientSettings.")
|
||||
alternativeValue
|
||||
}
|
||||
}
|
||||
@ -65,7 +65,7 @@ object ClientSettings extends SystemConfiguration {
|
||||
getConfigPropertyValueByPath(map, path) match {
|
||||
case Some(configValue: String) => configValue
|
||||
case _ =>
|
||||
logger.debug("Config `{}` not found.", path)
|
||||
logger.debug(s"Config `$path` with type String not found in clientSettings.")
|
||||
alternativeValue
|
||||
}
|
||||
}
|
||||
@ -74,7 +74,7 @@ object ClientSettings extends SystemConfiguration {
|
||||
getConfigPropertyValueByPath(map, path) match {
|
||||
case Some(configValue: Boolean) => configValue
|
||||
case _ =>
|
||||
logger.debug("Config `{}` not found.", path)
|
||||
logger.debug(s"Config `$path` with type Boolean found in clientSettings.")
|
||||
alternativeValue
|
||||
}
|
||||
}
|
||||
|
@ -189,9 +189,10 @@ class BigBlueButtonActor(
|
||||
context.stop(m.actorRef)
|
||||
}
|
||||
|
||||
MeetingDAO.delete(msg.meetingId)
|
||||
// MeetingDAO.delete(msg.meetingId)
|
||||
// MeetingDAO.setMeetingEnded(msg.meetingId)
|
||||
// Removing the meeting is enough, all other tables has "ON DELETE CASCADE"
|
||||
// UserDAO.deleteAllFromMeeting(msg.meetingId)
|
||||
// UserDAO.softDeleteAllFromMeeting(msg.meetingId)
|
||||
// MeetingRecordingDAO.updateStopped(msg.meetingId, "")
|
||||
|
||||
//Remove ColorPicker idx of the meeting
|
||||
|
@ -30,7 +30,7 @@ trait EjectUserFromBreakoutInternalMsgHdlr {
|
||||
)
|
||||
|
||||
//TODO inform reason
|
||||
UserDAO.delete(registeredUser.id)
|
||||
UserDAO.softDelete(registeredUser.id)
|
||||
|
||||
// send a system message to force disconnection
|
||||
Sender.sendDisconnectClientSysMsg(msg.breakoutId, registeredUser.id, msg.ejectedBy, msg.reasonCode, outGW)
|
||||
|
@ -40,7 +40,7 @@ trait UserLeftVoiceConfEvtMsgHdlr {
|
||||
UsersApp.guestWaitingLeft(liveMeeting, user.intId, outGW)
|
||||
}
|
||||
Users2x.remove(liveMeeting.users2x, user.intId)
|
||||
UserDAO.delete(user.intId)
|
||||
UserDAO.softDelete(user.intId)
|
||||
VoiceApp.removeUserFromVoiceConf(liveMeeting, outGW, msg.body.voiceUserId)
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,10 @@ case class MeetingDbModel(
|
||||
bannerText: Option[String],
|
||||
bannerColor: Option[String],
|
||||
createdTime: Long,
|
||||
durationInSeconds: Int
|
||||
durationInSeconds: Int,
|
||||
endedAt: Option[java.sql.Timestamp],
|
||||
endedReasonCode: Option[String],
|
||||
endedBy: Option[String],
|
||||
)
|
||||
|
||||
class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meeting") {
|
||||
@ -45,7 +48,10 @@ class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meet
|
||||
bannerText,
|
||||
bannerColor,
|
||||
createdTime,
|
||||
durationInSeconds
|
||||
durationInSeconds,
|
||||
endedAt,
|
||||
endedReasonCode,
|
||||
endedBy
|
||||
) <> (MeetingDbModel.tupled, MeetingDbModel.unapply)
|
||||
val meetingId = column[String]("meetingId", O.PrimaryKey)
|
||||
val extId = column[String]("extId")
|
||||
@ -64,6 +70,9 @@ class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meet
|
||||
val bannerColor = column[Option[String]]("bannerColor")
|
||||
val createdTime = column[Long]("createdTime")
|
||||
val durationInSeconds = column[Int]("durationInSeconds")
|
||||
val endedAt = column[Option[java.sql.Timestamp]]("endedAt")
|
||||
val endedReasonCode = column[Option[String]]("endedReasonCode")
|
||||
val endedBy = column[Option[String]]("endedBy")
|
||||
}
|
||||
|
||||
object MeetingDAO {
|
||||
@ -84,39 +93,42 @@ object MeetingDAO {
|
||||
learningDashboardAccessToken = meetingProps.password.learningDashboardAccessToken,
|
||||
logoutUrl = meetingProps.systemProps.logoutUrl,
|
||||
customLogoUrl = meetingProps.systemProps.customLogoURL match {
|
||||
case "" => None
|
||||
case "" => None
|
||||
case logoUrl => Some(logoUrl)
|
||||
},
|
||||
bannerText = meetingProps.systemProps.bannerText match {
|
||||
case "" => None
|
||||
case "" => None
|
||||
case bannerText => Some(bannerText)
|
||||
},
|
||||
bannerColor = meetingProps.systemProps.bannerColor match {
|
||||
case "" => None
|
||||
case "" => None
|
||||
case bannerColor => Some(bannerColor)
|
||||
},
|
||||
createdTime = meetingProps.durationProps.createdTime,
|
||||
durationInSeconds = meetingProps.durationProps.duration * 60
|
||||
durationInSeconds = meetingProps.durationProps.duration * 60,
|
||||
endedAt = None,
|
||||
endedReasonCode = None,
|
||||
endedBy = None
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => {
|
||||
DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted in Meeting table!")
|
||||
ChatDAO.insert(meetingProps.meetingProp.intId, GroupChatApp.createDefaultPublicGroupChat())
|
||||
MeetingUsersPoliciesDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp)
|
||||
MeetingLockSettingsDAO.insert(meetingProps.meetingProp.intId, meetingProps.lockSettingsProps)
|
||||
MeetingMetadataDAO.insert(meetingProps.meetingProp.intId, meetingProps.metadataProp)
|
||||
MeetingRecordingPoliciesDAO.insert(meetingProps.meetingProp.intId, meetingProps.recordProp)
|
||||
MeetingVoiceDAO.insert(meetingProps.meetingProp.intId, meetingProps.voiceProp)
|
||||
MeetingWelcomeDAO.insert(meetingProps.meetingProp.intId, meetingProps.welcomeProp)
|
||||
MeetingGroupDAO.insert(meetingProps.meetingProp.intId, meetingProps.groups)
|
||||
MeetingBreakoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.breakoutProps)
|
||||
TimerDAO.insert(meetingProps.meetingProp.intId)
|
||||
LayoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp.meetingLayout)
|
||||
MeetingClientSettingsDAO.insert(meetingProps.meetingProp.intId, JsonUtils.mapToJson(clientSettings))
|
||||
}
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error inserting Meeting: $e")
|
||||
case Success(rowsAffected) => {
|
||||
DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted in Meeting table!")
|
||||
ChatDAO.insert(meetingProps.meetingProp.intId, GroupChatApp.createDefaultPublicGroupChat())
|
||||
MeetingUsersPoliciesDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp)
|
||||
MeetingLockSettingsDAO.insert(meetingProps.meetingProp.intId, meetingProps.lockSettingsProps)
|
||||
MeetingMetadataDAO.insert(meetingProps.meetingProp.intId, meetingProps.metadataProp)
|
||||
MeetingRecordingPoliciesDAO.insert(meetingProps.meetingProp.intId, meetingProps.recordProp)
|
||||
MeetingVoiceDAO.insert(meetingProps.meetingProp.intId, meetingProps.voiceProp)
|
||||
MeetingWelcomeDAO.insert(meetingProps.meetingProp.intId, meetingProps.welcomeProp)
|
||||
MeetingGroupDAO.insert(meetingProps.meetingProp.intId, meetingProps.groups)
|
||||
MeetingBreakoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.breakoutProps)
|
||||
TimerDAO.insert(meetingProps.meetingProp.intId)
|
||||
LayoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp.meetingLayout)
|
||||
MeetingClientSettingsDAO.insert(meetingProps.meetingProp.intId, JsonUtils.mapToJson(clientSettings))
|
||||
}
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error inserting Meeting: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def updateMeetingDurationByParentMeeting(parentMeetingId: String, newDurationInSeconds: Int) = {
|
||||
@ -131,9 +143,9 @@ object MeetingDAO {
|
||||
.map(u => u.durationInSeconds)
|
||||
.update(newDurationInSeconds)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated durationInSeconds on Meeting table")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating durationInSeconds on Meeting: $e")
|
||||
}
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated durationInSeconds on Meeting table")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating durationInSeconds on Meeting: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def delete(meetingId: String) = {
|
||||
@ -142,9 +154,32 @@ object MeetingDAO {
|
||||
.filter(_.meetingId === meetingId)
|
||||
.delete
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"Meeting ${meetingId} deleted")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error deleting meeting ${meetingId}: $e")
|
||||
}
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"Meeting ${meetingId} deleted")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error deleting meeting ${meetingId}: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def setMeetingEnded(meetingId: String, endedReasonCode: String, endedBy: String) = {
|
||||
|
||||
UserDAO.softDeleteAllFromMeeting(meetingId)
|
||||
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[MeetingDbTableDef]
|
||||
.filter(_.meetingId === meetingId)
|
||||
.map(a => (a.endedAt, a.endedReasonCode, a.endedBy))
|
||||
.update(
|
||||
(
|
||||
Some(new java.sql.Timestamp(System.currentTimeMillis())),
|
||||
Some(endedReasonCode),
|
||||
endedBy match {
|
||||
case "" => None
|
||||
case c => Some(c)
|
||||
}
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated endedAt=now() on Meeting table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating endedAt=now() Meeting: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -135,7 +135,7 @@ object UserDAO {
|
||||
}
|
||||
|
||||
|
||||
def delete(intId: String) = {
|
||||
def softDelete(intId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserDbTableDef]
|
||||
.filter(_.userId === intId)
|
||||
@ -147,7 +147,19 @@ object UserDAO {
|
||||
}
|
||||
}
|
||||
|
||||
def deleteAllFromMeeting(meetingId: String) = {
|
||||
def softDeleteAllFromMeeting(meetingId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserDbTableDef]
|
||||
.filter(_.meetingId === meetingId)
|
||||
.map(u => (u.loggedOut))
|
||||
.update((true))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated loggedOut=true on user table!")
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error updating loggedOut=true user: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def permanentlyDeleteAllFromMeeting(meetingId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserDbTableDef]
|
||||
.filter(_.meetingId === meetingId)
|
||||
|
@ -122,7 +122,7 @@ object RegisteredUsers {
|
||||
u
|
||||
} else {
|
||||
users.delete(ejectedUser.id)
|
||||
// UserDAO.delete(ejectedUser) it's being removed in User2x already
|
||||
// UserDAO.softDelete(ejectedUser) it's being removed in User2x already
|
||||
ejectedUser
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ object Users2x {
|
||||
}
|
||||
|
||||
def remove(users: Users2x, intId: String): Option[UserState] = {
|
||||
//UserDAO.delete(intId)
|
||||
//UserDAO.softDelete(intId)
|
||||
users.remove(intId)
|
||||
}
|
||||
|
||||
@ -125,7 +125,7 @@ object Users2x {
|
||||
_ <- users.remove(intId)
|
||||
ejectedUser <- users.removeFromCache(intId)
|
||||
} yield {
|
||||
// UserDAO.delete(intId) --it will keep the user on Db
|
||||
// UserDAO.softDelete(intId) --it will keep the user on Db
|
||||
ejectedUser
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import org.bigbluebutton.core.apps.groupchats.GroupChatApp
|
||||
import org.bigbluebutton.core.apps.users.UsersApp
|
||||
import org.bigbluebutton.core.apps.voice.VoiceApp
|
||||
import org.bigbluebutton.core.bus.{BigBlueButtonEvent, InternalEventBus}
|
||||
import org.bigbluebutton.core.db.{BreakoutRoomUserDAO, MeetingRecordingDAO, UserBreakoutRoomDAO}
|
||||
import org.bigbluebutton.core.db.{BreakoutRoomUserDAO, MeetingDAO, MeetingRecordingDAO, UserBreakoutRoomDAO}
|
||||
import org.bigbluebutton.core.domain.{MeetingEndReason, MeetingState2x}
|
||||
import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core2.MeetingStatus2x
|
||||
@ -206,6 +206,8 @@ trait HandlerHelpers extends SystemConfiguration {
|
||||
|
||||
val endedEvnt = buildMeetingEndedEvtMsg(liveMeeting.props.meetingProp.intId)
|
||||
outGW.send(endedEvnt)
|
||||
|
||||
MeetingDAO.setMeetingEnded(liveMeeting.props.meetingProp.intId, reason, userId)
|
||||
}
|
||||
|
||||
def destroyMeeting(eventBus: InternalEventBus, meetingId: String): Unit = {
|
||||
|
@ -75,6 +75,7 @@ public class MeetingService implements MessageListener {
|
||||
*/
|
||||
private final ConcurrentMap<String, Meeting> meetings;
|
||||
private final ConcurrentMap<String, UserSession> sessions;
|
||||
private final ConcurrentMap<String, UserSessionBasicData> removedSessions;
|
||||
|
||||
private RecordingService recordingService;
|
||||
private LearningDashboardService learningDashboardService;
|
||||
@ -88,6 +89,7 @@ public class MeetingService implements MessageListener {
|
||||
|
||||
private long usersTimeout;
|
||||
private long waitingGuestUsersTimeout;
|
||||
private int sessionsCleanupDelayInMinutes;
|
||||
private long enteredUsersTimeout;
|
||||
|
||||
private ParamsProcessorUtil paramsProcessorUtil;
|
||||
@ -100,6 +102,7 @@ public class MeetingService implements MessageListener {
|
||||
public MeetingService() {
|
||||
meetings = new ConcurrentHashMap<String, Meeting>(8, 0.9f, 1);
|
||||
sessions = new ConcurrentHashMap<String, UserSession>(8, 0.9f, 1);
|
||||
removedSessions = new ConcurrentHashMap<String, UserSessionBasicData>(8, 0.9f, 1);
|
||||
uploadAuthzTokens = new HashMap<String, PresentationUploadToken>();
|
||||
}
|
||||
|
||||
@ -149,12 +152,16 @@ public class MeetingService implements MessageListener {
|
||||
return null;
|
||||
}
|
||||
|
||||
public UserSession getUserSessionWithAuthToken(String token) {
|
||||
public UserSession getUserSessionWithSessionToken(String token) {
|
||||
return sessions.get(token);
|
||||
}
|
||||
|
||||
public UserSessionBasicData getRemovedUserSessionWithSessionToken(String sessionToken) {
|
||||
return removedSessions.get(sessionToken);
|
||||
}
|
||||
|
||||
public Boolean getAllowRequestsWithoutSession(String token) {
|
||||
UserSession us = getUserSessionWithAuthToken(token);
|
||||
UserSession us = getUserSessionWithSessionToken(token);
|
||||
if (us == null) {
|
||||
return false;
|
||||
} else {
|
||||
@ -164,12 +171,21 @@ public class MeetingService implements MessageListener {
|
||||
}
|
||||
}
|
||||
|
||||
public UserSession removeUserSessionWithAuthToken(String token) {
|
||||
UserSession user = sessions.remove(token);
|
||||
if (user != null) {
|
||||
log.debug("Found user {} token={} to meeting {}", user.fullname, token, user.meetingID);
|
||||
public void removeUserSessionWithSessionToken(String token) {
|
||||
log.debug("Removing token={}", token);
|
||||
UserSession us = getUserSessionWithSessionToken(token);
|
||||
if (us != null) {
|
||||
log.debug("Found user {} token={} to meeting {}", us.fullname, token, us.meetingID);
|
||||
|
||||
UserSessionBasicData removedUser = new UserSessionBasicData();
|
||||
removedUser.meetingId = us.meetingID;
|
||||
removedUser.userId = us.internalUserId;
|
||||
removedUser.sessionToken = us.authToken;
|
||||
removedSessions.put(token, removedUser);
|
||||
sessions.remove(token);
|
||||
} else {
|
||||
log.debug("Not found token={}", token);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -295,16 +311,40 @@ public class MeetingService implements MessageListener {
|
||||
notifier.sendUploadFileTooLargeMessage(presUploadToken, uploadedFileSize, maxUploadFileSize);
|
||||
}
|
||||
|
||||
private void removeUserSessions(String meetingId) {
|
||||
Iterator<Map.Entry<String, UserSession>> iterator = sessions.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, UserSession> entry = iterator.next();
|
||||
UserSession userSession = entry.getValue();
|
||||
|
||||
private void removeUserSessionsFromMeeting(String meetingId) {
|
||||
for (String token : sessions.keySet()) {
|
||||
UserSession userSession = sessions.get(token);
|
||||
if (userSession.meetingID.equals(meetingId)) {
|
||||
iterator.remove();
|
||||
System.out.println(token + " = " + userSession.authToken);
|
||||
removeUserSessionWithSessionToken(token);
|
||||
}
|
||||
}
|
||||
|
||||
scheduleRemovedSessionsCleanUp(meetingId);
|
||||
}
|
||||
|
||||
private void scheduleRemovedSessionsCleanUp(String meetingId) {
|
||||
Calendar cleanUpDelayCalendar = Calendar.getInstance();
|
||||
cleanUpDelayCalendar.add(Calendar.MINUTE, sessionsCleanupDelayInMinutes);
|
||||
|
||||
log.debug("Sessions for meeting={} will be removed within {} minutes.", meetingId, sessionsCleanupDelayInMinutes);
|
||||
new java.util.Timer().schedule(
|
||||
new java.util.TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
Iterator<Map.Entry<String, UserSessionBasicData>> iterator = removedSessions.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, UserSessionBasicData> entry = iterator.next();
|
||||
UserSessionBasicData removedUserSession = entry.getValue();
|
||||
|
||||
if (removedUserSession.meetingId.equals(meetingId)) {
|
||||
log.debug("Removed user {} session for meeting {}.",removedUserSession.userId, removedUserSession.meetingId);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, cleanUpDelayCalendar.getTime()
|
||||
);
|
||||
}
|
||||
|
||||
private void destroyMeeting(String meetingId) {
|
||||
@ -703,7 +743,7 @@ public class MeetingService implements MessageListener {
|
||||
}
|
||||
destroyMeeting(m.getInternalId());
|
||||
meetings.remove(m.getInternalId());
|
||||
removeUserSessions(m.getInternalId());
|
||||
removeUserSessionsFromMeeting(m.getInternalId());
|
||||
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", m.getInternalId());
|
||||
@ -1111,7 +1151,7 @@ public class MeetingService implements MessageListener {
|
||||
user.setRole(message.role);
|
||||
String sessionToken = getTokenByUserId(user.getInternalUserId());
|
||||
if (sessionToken != null) {
|
||||
UserSession userSession = getUserSessionWithAuthToken(sessionToken);
|
||||
UserSession userSession = getUserSessionWithSessionToken(sessionToken);
|
||||
userSession.role = message.role;
|
||||
sessions.replace(sessionToken, userSession);
|
||||
}
|
||||
@ -1363,6 +1403,10 @@ public class MeetingService implements MessageListener {
|
||||
waitingGuestUsersTimeout = value;
|
||||
}
|
||||
|
||||
public void setSessionsCleanupDelayInMinutes(int value) {
|
||||
sessionsCleanupDelayInMinutes = value;
|
||||
}
|
||||
|
||||
public void setEnteredUsersTimeout(long value) {
|
||||
enteredUsersTimeout = value;
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2012 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.api.domain;
|
||||
|
||||
public class UserSessionBasicData {
|
||||
public String sessionToken = null;
|
||||
public String userId = null;
|
||||
public String meetingId = null;
|
||||
|
||||
public String toString() {
|
||||
return meetingId + " " + userId + " " + sessionToken;
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ public class GuestPolicyValidator implements ConstraintValidator<GuestPolicyCons
|
||||
}
|
||||
|
||||
MeetingService meetingService = ServiceUtils.getMeetingService();
|
||||
UserSession userSession = meetingService.getUserSessionWithAuthToken(sessionToken);
|
||||
UserSession userSession = meetingService.getUserSessionWithSessionToken(sessionToken);
|
||||
|
||||
if(userSession == null || !userSession.guestStatus.equals(GuestPolicy.ALLOW)) {
|
||||
return false;
|
||||
|
@ -19,7 +19,7 @@ public class UserSessionValidator implements ConstraintValidator<UserSessionCons
|
||||
return false;
|
||||
}
|
||||
|
||||
UserSession userSession = ServiceUtils.getMeetingService().getUserSessionWithAuthToken(sessionToken);
|
||||
UserSession userSession = ServiceUtils.getMeetingService().getUserSessionWithSessionToken(sessionToken);
|
||||
|
||||
if(userSession == null) {
|
||||
return false;
|
||||
|
@ -22,7 +22,7 @@ public class SessionService {
|
||||
|
||||
private void getUserSessionWithToken() {
|
||||
if(sessionToken != null) {
|
||||
userSession = meetingService.getUserSessionWithAuthToken(sessionToken);
|
||||
userSession = meetingService.getUserSessionWithSessionToken(sessionToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { RedisMessage } from '../types';
|
||||
import { ValidationError } from '../types/ValidationError';
|
||||
import {throwErrorIfNotPresenter} from "../imports/validation";
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
throwErrorIfNotPresenter(sessionVariables);
|
||||
const eventName = `DeleteWhiteboardAnnotationsPubMsg`;
|
||||
|
||||
const routing = {
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { RedisMessage } from '../types';
|
||||
import { ValidationError } from '../types/ValidationError';
|
||||
import {throwErrorIfNotPresenter} from "../imports/validation";
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
throwErrorIfNotPresenter(sessionVariables);
|
||||
const eventName = `DeleteWhiteboardAnnotationsPubMsg`;
|
||||
|
||||
const routing = {
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { RedisMessage } from '../types';
|
||||
import { ValidationError } from '../types/ValidationError';
|
||||
import {throwErrorIfNotPresenter} from "../imports/validation";
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
throwErrorIfNotPresenter(sessionVariables);
|
||||
const eventName = `SendWhiteboardAnnotationsPubMsg`;
|
||||
|
||||
const routing = {
|
||||
|
27
bbb-graphql-actions/src/actions/presentationExport.ts
Normal file
27
bbb-graphql-actions/src/actions/presentationExport.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { RedisMessage } from '../types';
|
||||
import {throwErrorIfNotPresenter} from "../imports/validation";
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
throwErrorIfNotPresenter(sessionVariables);
|
||||
const eventName = `MakePresentationDownloadReqMsg`;
|
||||
|
||||
const routing = {
|
||||
meetingId: sessionVariables['x-hasura-meetingid'] as String,
|
||||
userId: sessionVariables['x-hasura-userid'] as String
|
||||
};
|
||||
|
||||
const header = {
|
||||
name: eventName,
|
||||
meetingId: routing.meetingId,
|
||||
userId: routing.userId
|
||||
};
|
||||
|
||||
const body = {
|
||||
presId: input.presentationId,
|
||||
allPages: true,
|
||||
fileStateType: input.fileStateType,
|
||||
pages: [],
|
||||
};
|
||||
|
||||
return { eventName, routing, header, body };
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import { RedisMessage } from '../types';
|
||||
import { ValidationError } from '../types/ValidationError';
|
||||
import {throwErrorIfNotPresenter} from "../imports/validation";
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
|
@ -89,6 +89,7 @@ export default function Auth() {
|
||||
}
|
||||
meeting {
|
||||
name
|
||||
ended
|
||||
}
|
||||
}
|
||||
}`
|
||||
@ -107,7 +108,12 @@ export default function Auth() {
|
||||
{data.user_current.map((curr) => {
|
||||
console.log('user_current', curr);
|
||||
|
||||
if(curr.loggedOut) {
|
||||
if(curr.meeting.ended) {
|
||||
return <div>
|
||||
{curr.meeting.name}
|
||||
<br/><br/>
|
||||
Meeting has ended.</div>
|
||||
} else if(curr.ejected) {
|
||||
return <div>
|
||||
{curr.meeting.name}
|
||||
<br/><br/>
|
||||
@ -139,11 +145,11 @@ export default function Auth() {
|
||||
<span>You are online, welcome {curr.name} ({curr.userId})</span>
|
||||
<button onClick={() => handleDispatchUserLeave()}>Leave Now!</button>
|
||||
|
||||
{/*<MyInfo userAuthToken={curr.authToken} />*/}
|
||||
{/*<br />*/}
|
||||
<MyInfo userAuthToken={curr.authToken} />
|
||||
<br />
|
||||
|
||||
{/*<MeetingInfo />*/}
|
||||
{/*<br />*/}
|
||||
<MeetingInfo />
|
||||
<br />
|
||||
|
||||
<TotalOfUsers />
|
||||
<TotalOfModerators />
|
||||
|
@ -1,8 +1,10 @@
|
||||
import {gql, useMutation, useSubscription} from '@apollo/client';
|
||||
import React, {useEffect} from "react";
|
||||
import React, {useEffect, useState, useRef } from "react";
|
||||
import {applyPatch} from "fast-json-patch";
|
||||
|
||||
export default function UserConnectionStatus() {
|
||||
const networkRttInMs = useRef(null); // Ref to store the current timeout
|
||||
const lastStatusUpdatedAtReceived = useRef(null); // Ref to store the current timeout
|
||||
|
||||
//example specifying where and time (new Date().toISOString())
|
||||
//but its not necessary
|
||||
@ -18,13 +20,20 @@ export default function UserConnectionStatus() {
|
||||
// `);
|
||||
|
||||
|
||||
const timeoutRef = useRef(null); // Ref to store the current timeout
|
||||
|
||||
|
||||
|
||||
//where is not necessary once user can update only its own status
|
||||
//Hasura accepts "now()" as value to timestamp fields
|
||||
const [updateUserClientResponseAtToMeAsNow] = useMutation(gql`
|
||||
mutation UpdateConnectionAliveAt($userId: String, $userClientResponseAt: timestamp) {
|
||||
mutation UpdateConnectionClientResponse($networkRttInMs: numeric) {
|
||||
update_user_connectionStatus(
|
||||
where: {userClientResponseAt: {_is_null: true}}
|
||||
_set: { userClientResponseAt: "now()" }
|
||||
_set: {
|
||||
userClientResponseAt: "now()",
|
||||
networkRttInMs: $networkRttInMs
|
||||
}
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
@ -32,7 +41,11 @@ export default function UserConnectionStatus() {
|
||||
`);
|
||||
|
||||
const handleUpdateUserClientResponseAt = () => {
|
||||
updateUserClientResponseAtToMeAsNow();
|
||||
updateUserClientResponseAtToMeAsNow({
|
||||
variables: {
|
||||
networkRttInMs: networkRttInMs.current
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -48,11 +61,25 @@ export default function UserConnectionStatus() {
|
||||
`);
|
||||
|
||||
const handleUpdateConnectionAliveAt = () => {
|
||||
updateConnectionAliveAtToMeAsNow();
|
||||
const startTime = performance.now();
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
updateConnectionAliveAtToMeAsNow().then(result => {
|
||||
const endTime = performance.now();
|
||||
networkRttInMs.current = endTime - startTime;
|
||||
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error performing mutation:', error);
|
||||
}
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
handleUpdateConnectionAliveAt();
|
||||
}, 25000);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -66,7 +93,8 @@ export default function UserConnectionStatus() {
|
||||
user_connectionStatus {
|
||||
connectionAliveAt
|
||||
userClientResponseAt
|
||||
rttInMs
|
||||
applicationRttInMs
|
||||
networkRttInMs
|
||||
status
|
||||
statusUpdatedAt
|
||||
}
|
||||
@ -83,7 +111,8 @@ export default function UserConnectionStatus() {
|
||||
{/*<th>Id</th>*/}
|
||||
<th>connectionAliveAt</th>
|
||||
<th>userClientResponseAt</th>
|
||||
<th>rttInMs</th>
|
||||
<th>applicationRttInMs</th>
|
||||
<th>networkRttInMs</th>
|
||||
<th>status</th>
|
||||
<th>statusUpdatedAt</th>
|
||||
</tr>
|
||||
@ -92,12 +121,17 @@ export default function UserConnectionStatus() {
|
||||
{data.user_connectionStatus.map((curr) => {
|
||||
// console.log('user_connectionStatus', curr);
|
||||
|
||||
if(curr.userClientResponseAt == null) {
|
||||
// handleUpdateUserClientResponseAt();
|
||||
const delay = 500;
|
||||
setTimeout(() => {
|
||||
handleUpdateUserClientResponseAt();
|
||||
},delay);
|
||||
console.log('curr.statusUpdatedAt',curr.statusUpdatedAt);
|
||||
console.log('lastStatusUpdatedAtReceived.current',lastStatusUpdatedAtReceived.current);
|
||||
|
||||
if(curr.userClientResponseAt == null
|
||||
&& (curr.statusUpdatedAt == null || curr.statusUpdatedAt !== lastStatusUpdatedAtReceived.current)) {
|
||||
|
||||
|
||||
|
||||
lastStatusUpdatedAtReceived.current = curr.statusUpdatedAt;
|
||||
// setLastStatusUpdatedAtReceived(curr.statusUpdatedAt);
|
||||
handleUpdateUserClientResponseAt();
|
||||
}
|
||||
|
||||
return (
|
||||
@ -106,7 +140,8 @@ export default function UserConnectionStatus() {
|
||||
<button onClick={() => handleUpdateConnectionAliveAt()}>Update now!</button>
|
||||
</td>
|
||||
<td>{curr.userClientResponseAt}</td>
|
||||
<td>{curr.rttInMs}</td>
|
||||
<td>{curr.applicationRttInMs}</td>
|
||||
<td>{curr.networkRttInMs}</td>
|
||||
<td>{curr.status}</td>
|
||||
<td>{curr.statusUpdatedAt}</td>
|
||||
</tr>
|
||||
|
@ -33,7 +33,7 @@ export default function UserConnectionStatusReport() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.user_connectionStatusReport.map((curr) => {
|
||||
console.log('user_connectionStatusReport', curr);
|
||||
//console.log('user_connectionStatusReport', curr);
|
||||
return (
|
||||
<tr key={curr.user.userId}>
|
||||
<td>{curr.user.name}</td>
|
||||
|
@ -25,6 +25,7 @@ type GraphQlSubscription struct {
|
||||
StreamCursorField string
|
||||
StreamCursorVariableName string
|
||||
StreamCursorCurrValue interface{}
|
||||
LastReceivedDataChecksum uint32
|
||||
JsonPatchSupported bool // indicate if client support Json Patch for this subscription
|
||||
LastSeenOnHasuraConnection string // id of the hasura connection that this query was active
|
||||
}
|
||||
@ -42,9 +43,10 @@ type BrowserConnection struct {
|
||||
}
|
||||
|
||||
type HasuraConnection struct {
|
||||
Id string // hasura connection id
|
||||
Browserconn *BrowserConnection // browser connection that originated this hasura connection
|
||||
Websocket *websocket.Conn // websocket used to connect to hasura
|
||||
Context context.Context // hasura connection context (child of browser connection context)
|
||||
ContextCancelFunc context.CancelFunc // function to cancel the hasura context (and so, the hasura connection)
|
||||
Id string // hasura connection id
|
||||
Browserconn *BrowserConnection // browser connection that originated this hasura connection
|
||||
Websocket *websocket.Conn // websocket used to connect to hasura
|
||||
Context context.Context // hasura connection context (child of browser connection context)
|
||||
ContextCancelFunc context.CancelFunc // function to cancel the hasura context (and so, the hasura connection)
|
||||
MsgReceivingActiveChan *SafeChannel // indicate that it's waiting for the return of mutations before closing connection
|
||||
}
|
||||
|
@ -60,10 +60,11 @@ func HasuraClient(browserConnection *common.BrowserConnection, cookies []*http.C
|
||||
defer hasuraConnectionContextCancel()
|
||||
|
||||
var thisConnection = common.HasuraConnection{
|
||||
Id: hasuraConnectionId,
|
||||
Browserconn: browserConnection,
|
||||
Context: hasuraConnectionContext,
|
||||
ContextCancelFunc: hasuraConnectionContextCancel,
|
||||
Id: hasuraConnectionId,
|
||||
Browserconn: browserConnection,
|
||||
Context: hasuraConnectionContext,
|
||||
ContextCancelFunc: hasuraConnectionContextCancel,
|
||||
MsgReceivingActiveChan: common.NewSafeChannel(1),
|
||||
}
|
||||
|
||||
browserConnection.HasuraConnection = &thisConnection
|
||||
|
@ -2,11 +2,13 @@ package reader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/common"
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/hascli/retransmiter"
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/msgpatch"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"hash/crc32"
|
||||
"nhooyr.io/websocket/wsjson"
|
||||
"sync"
|
||||
)
|
||||
@ -35,68 +37,118 @@ func HasuraConnectionReader(hc *common.HasuraConnection, fromHasuraToBrowserChan
|
||||
|
||||
log.Tracef("received from hasura: %v", message)
|
||||
|
||||
var messageAsMap = message.(map[string]interface{})
|
||||
handleMessageReceivedFromHasura(hc, fromHasuraToBrowserChannel, fromBrowserToHasuraChannel, message)
|
||||
}
|
||||
}
|
||||
|
||||
if messageAsMap != nil {
|
||||
var messageType = messageAsMap["type"]
|
||||
var queryId, _ = messageAsMap["id"].(string)
|
||||
func handleMessageReceivedFromHasura(hc *common.HasuraConnection, fromHasuraToBrowserChannel *common.SafeChannel, fromBrowserToHasuraChannel *common.SafeChannel, message interface{}) {
|
||||
var messageMap = message.(map[string]interface{})
|
||||
|
||||
//Check if subscription is still active!
|
||||
if queryId != "" {
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.RLock()
|
||||
subscription, ok := hc.Browserconn.ActiveSubscriptions[queryId]
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.RUnlock()
|
||||
if !ok {
|
||||
log.Debugf("Subscription with Id %s doesn't exist anymore, skiping response.", queryId)
|
||||
if messageMap != nil {
|
||||
var messageType = messageMap["type"]
|
||||
var queryId, _ = messageMap["id"].(string)
|
||||
|
||||
//Check if subscription is still active!
|
||||
if queryId != "" {
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.RLock()
|
||||
subscription, ok := hc.Browserconn.ActiveSubscriptions[queryId]
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.RUnlock()
|
||||
if !ok {
|
||||
log.Debugf("Subscription with Id %s doesn't exist anymore, skiping response.", queryId)
|
||||
return
|
||||
}
|
||||
|
||||
//When Hasura send msg type "complete", this query is finished
|
||||
if messageType == "complete" {
|
||||
handleCompleteMessage(hc, queryId)
|
||||
}
|
||||
|
||||
if messageType == "data" &&
|
||||
subscription.Type == common.Subscription {
|
||||
hasNoPreviousOccurrence := handleSubscriptionMessage(hc, messageMap, subscription, queryId)
|
||||
|
||||
if !hasNoPreviousOccurrence {
|
||||
return
|
||||
}
|
||||
|
||||
//When Hasura send msg type "complete", this query is finished
|
||||
if messageType == "complete" {
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.Lock()
|
||||
delete(hc.Browserconn.ActiveSubscriptions, queryId)
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.Unlock()
|
||||
log.Debugf("Subscription with Id %s finished by Hasura.", queryId)
|
||||
}
|
||||
|
||||
//Apply msg patch when it supports it
|
||||
if subscription.JsonPatchSupported &&
|
||||
messageType == "data" &&
|
||||
subscription.Type == common.Subscription {
|
||||
msgpatch.PatchMessage(&messageAsMap, hc.Browserconn)
|
||||
}
|
||||
|
||||
//Set last cursor value for stream
|
||||
if subscription.Type == common.Streaming {
|
||||
lastCursor := common.GetLastStreamCursorValueFromReceivedMessage(messageAsMap, subscription.StreamCursorField)
|
||||
if lastCursor != nil && subscription.StreamCursorCurrValue != lastCursor {
|
||||
subscription.StreamCursorCurrValue = lastCursor
|
||||
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.Lock()
|
||||
hc.Browserconn.ActiveSubscriptions[queryId] = subscription
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.Unlock()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Retransmit the subscription start commands when hasura confirms the connection
|
||||
// this is useful in case of a connection invalidation
|
||||
if messageType == "connection_ack" {
|
||||
//Hasura connection was initialized, now it's able to send new messages to Hasura
|
||||
fromBrowserToHasuraChannel.UnfreezeChannel()
|
||||
|
||||
//Avoid to send `connection_ack` to the browser when it's a reconnection
|
||||
if hc.Browserconn.ConnAckSentToBrowser == false {
|
||||
fromHasuraToBrowserChannel.Send(messageAsMap)
|
||||
hc.Browserconn.ConnAckSentToBrowser = true
|
||||
}
|
||||
|
||||
go retransmiter.RetransmitSubscriptionStartMessages(hc, fromBrowserToHasuraChannel)
|
||||
} else {
|
||||
// Forward the message to browser
|
||||
fromHasuraToBrowserChannel.Send(messageAsMap)
|
||||
//Set last cursor value for stream
|
||||
if subscription.Type == common.Streaming {
|
||||
handleStreamingMessage(hc, messageMap, subscription, queryId)
|
||||
}
|
||||
}
|
||||
|
||||
// Retransmit the subscription start commands when hasura confirms the connection
|
||||
// this is useful in case of a connection invalidation
|
||||
if messageType == "connection_ack" {
|
||||
handleConnectionAckMessage(hc, messageMap, fromHasuraToBrowserChannel, fromBrowserToHasuraChannel)
|
||||
} else {
|
||||
// Forward the message to browser
|
||||
fromHasuraToBrowserChannel.Send(messageMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleSubscriptionMessage(hc *common.HasuraConnection, messageMap map[string]interface{}, subscription common.GraphQlSubscription, queryId string) bool {
|
||||
if payload, okPayload := messageMap["payload"].(map[string]interface{}); okPayload {
|
||||
if data, okData := payload["data"].(map[string]interface{}); okData {
|
||||
for dataKey, dataItem := range data {
|
||||
if currentDataProp, okCurrentDataProp := dataItem.([]interface{}); okCurrentDataProp {
|
||||
if dataAsJson, err := json.Marshal(currentDataProp); err == nil {
|
||||
//Check whether ReceivedData is different from the LastReceivedData
|
||||
//Otherwise stop forwarding this message
|
||||
dataChecksum := crc32.ChecksumIEEE(dataAsJson)
|
||||
if subscription.LastReceivedDataChecksum == dataChecksum {
|
||||
return false
|
||||
}
|
||||
|
||||
//Store LastReceivedData Checksum
|
||||
subscription.LastReceivedDataChecksum = dataChecksum
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.Lock()
|
||||
hc.Browserconn.ActiveSubscriptions[queryId] = subscription
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.Unlock()
|
||||
|
||||
//Apply msg patch when it supports it
|
||||
if subscription.JsonPatchSupported {
|
||||
msgpatch.PatchMessage(&messageMap, queryId, dataKey, dataAsJson, hc.Browserconn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func handleStreamingMessage(hc *common.HasuraConnection, messageMap map[string]interface{}, subscription common.GraphQlSubscription, queryId string) {
|
||||
lastCursor := common.GetLastStreamCursorValueFromReceivedMessage(messageMap, subscription.StreamCursorField)
|
||||
if lastCursor != nil && subscription.StreamCursorCurrValue != lastCursor {
|
||||
subscription.StreamCursorCurrValue = lastCursor
|
||||
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.Lock()
|
||||
hc.Browserconn.ActiveSubscriptions[queryId] = subscription
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func handleCompleteMessage(hc *common.HasuraConnection, queryId string) {
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.Lock()
|
||||
delete(hc.Browserconn.ActiveSubscriptions, queryId)
|
||||
hc.Browserconn.ActiveSubscriptionsMutex.Unlock()
|
||||
log.Debugf("Subscription with Id %s finished by Hasura.", queryId)
|
||||
}
|
||||
|
||||
func handleConnectionAckMessage(hc *common.HasuraConnection, messageMap map[string]interface{}, fromHasuraToBrowserChannel *common.SafeChannel, fromBrowserToHasuraChannel *common.SafeChannel) {
|
||||
log.Debugf("Received connection_ack")
|
||||
//Hasura connection was initialized, now it's able to send new messages to Hasura
|
||||
fromBrowserToHasuraChannel.UnfreezeChannel()
|
||||
|
||||
//Avoid to send `connection_ack` to the browser when it's a reconnection
|
||||
if hc.Browserconn.ConnAckSentToBrowser == false {
|
||||
fromHasuraToBrowserChannel.Send(messageMap)
|
||||
hc.Browserconn.ConnAckSentToBrowser = true
|
||||
}
|
||||
|
||||
go retransmiter.RetransmitSubscriptionStartMessages(hc, fromBrowserToHasuraChannel)
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
package writer
|
||||
|
||||
import (
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/msgpatch"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/common"
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/msgpatch"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"nhooyr.io/websocket/wsjson"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// HasuraConnectionWriter
|
||||
@ -39,6 +38,10 @@ RangeLoop:
|
||||
select {
|
||||
case <-hc.Context.Done():
|
||||
break RangeLoop
|
||||
case <-hc.MsgReceivingActiveChan.ReceiveChannel():
|
||||
log.Debugf("freezing channel fromBrowserToHasuraChannel")
|
||||
//Freeze channel once it's about to close Hasura connection
|
||||
fromBrowserToHasuraChannel.FreezeChannel()
|
||||
case fromBrowserMessage := <-fromBrowserToHasuraChannel.ReceiveChannel():
|
||||
{
|
||||
if fromBrowserMessage == nil {
|
||||
@ -52,6 +55,7 @@ RangeLoop:
|
||||
|
||||
//Identify type based on query string
|
||||
messageType := common.Query
|
||||
var lastReceivedDataChecksum uint32
|
||||
streamCursorField := ""
|
||||
streamCursorVariableName := ""
|
||||
var streamCursorInitialValue interface{}
|
||||
@ -62,12 +66,16 @@ RangeLoop:
|
||||
if strings.HasPrefix(query, "subscription") {
|
||||
messageType = common.Subscription
|
||||
|
||||
browserConnection.ActiveSubscriptionsMutex.RLock()
|
||||
existingSubscriptionData, queryIdExists := browserConnection.ActiveSubscriptions[queryId]
|
||||
browserConnection.ActiveSubscriptionsMutex.RUnlock()
|
||||
if queryIdExists {
|
||||
lastReceivedDataChecksum = existingSubscriptionData.LastReceivedDataChecksum
|
||||
}
|
||||
|
||||
if strings.Contains(query, "_stream(") && strings.Contains(query, "cursor: {") {
|
||||
messageType = common.Streaming
|
||||
|
||||
browserConnection.ActiveSubscriptionsMutex.RLock()
|
||||
_, queryIdExists := browserConnection.ActiveSubscriptions[queryId]
|
||||
browserConnection.ActiveSubscriptionsMutex.RUnlock()
|
||||
if !queryIdExists {
|
||||
streamCursorField, streamCursorVariableName, streamCursorInitialValue = common.GetStreamCursorPropsFromQuery(payload, query)
|
||||
|
||||
@ -107,6 +115,7 @@ RangeLoop:
|
||||
LastSeenOnHasuraConnection: hc.Id,
|
||||
JsonPatchSupported: jsonPatchSupported,
|
||||
Type: messageType,
|
||||
LastReceivedDataChecksum: lastReceivedDataChecksum,
|
||||
}
|
||||
// log.Tracef("Current queries: %v", browserConnection.ActiveSubscriptions)
|
||||
browserConnection.ActiveSubscriptionsMutex.Unlock()
|
||||
|
@ -82,95 +82,60 @@ func ClearAllCaches() {
|
||||
}
|
||||
}
|
||||
|
||||
func PatchMessage(receivedMessage *map[string]interface{}, bConn *common.BrowserConnection) {
|
||||
func PatchMessage(receivedMessage *map[string]interface{}, queryId string, dataKey string, dataAsJson []byte, bConn *common.BrowserConnection) {
|
||||
var receivedMessageMap = *receivedMessage
|
||||
|
||||
idValue, ok := receivedMessageMap["id"]
|
||||
if !ok {
|
||||
//Id does not exists in response Json
|
||||
//It's not a subscription data
|
||||
fileCacheDirPath, err := getSubscriptionCacheDirPath(bConn, queryId, true)
|
||||
if err != nil {
|
||||
log.Errorf("Error on get Client/Subscription cache path: %v", err)
|
||||
return
|
||||
}
|
||||
filePath := fileCacheDirPath + dataKey + ".json"
|
||||
|
||||
payload, ok := receivedMessageMap["payload"].(map[string]interface{})
|
||||
if !ok {
|
||||
//payload does not exists in response Json
|
||||
//It's not a subscription data
|
||||
return
|
||||
lastContent, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
//Last content doesn't exist, probably it's the first response
|
||||
}
|
||||
|
||||
data, ok := payload["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
//payload.data does not exists in response Json
|
||||
//It's not a subscription data
|
||||
return
|
||||
}
|
||||
for key, value := range data {
|
||||
currentData, ok := value.([]interface{})
|
||||
if !ok {
|
||||
log.Errorf("Payload/Data/%s does not exists in response Json.", key)
|
||||
return
|
||||
}
|
||||
|
||||
dataAsJsonString, err := json.Marshal(currentData)
|
||||
if err != nil {
|
||||
log.Errorf("Error on convert Payload/Data/%s.", key)
|
||||
return
|
||||
}
|
||||
|
||||
fileCacheDirPath, err := getSubscriptionCacheDirPath(bConn, idValue.(string), true)
|
||||
if err != nil {
|
||||
log.Errorf("Error on get Client/Subscription cache path: %v", err)
|
||||
return
|
||||
}
|
||||
filePath := fileCacheDirPath + key + ".json"
|
||||
|
||||
lastContent, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
//Last content doesn't exist, probably it's the first response
|
||||
}
|
||||
lastDataAsJsonString := string(lastContent)
|
||||
if string(dataAsJsonString) == lastDataAsJsonString {
|
||||
//Content didn't change, set message as null to avoid sending it to the browser
|
||||
//This case is usual when the middleware reconnects with Hasura and receives the data again
|
||||
*receivedMessage = nil
|
||||
} else {
|
||||
//Content was changed, creating json patch
|
||||
//If data is small (< minLengthToPatch) it's not worth creating the patch
|
||||
if lastDataAsJsonString != "" && len(string(dataAsJsonString)) > minLengthToPatch {
|
||||
diffPatch, e := jsonpatch.CreatePatch([]byte(lastDataAsJsonString), []byte(dataAsJsonString))
|
||||
if e != nil {
|
||||
log.Errorf("Error creating JSON patch:%v", e)
|
||||
return
|
||||
}
|
||||
jsonDiffPatch, err := json.Marshal(diffPatch)
|
||||
if err != nil {
|
||||
log.Errorf("Error marshaling patch array:", err)
|
||||
return
|
||||
}
|
||||
|
||||
//Use patch if the length is {minShrinkToUsePatch}% smaller than the original msg
|
||||
if float64(len(string(jsonDiffPatch)))/float64(len(string(dataAsJsonString))) < minShrinkToUsePatch {
|
||||
//Modify receivedMessage to include the Patch and remove the previous data
|
||||
//The key of the original message is kept to avoid errors (Apollo-client expects to receive this prop)
|
||||
receivedMessageMap["payload"] = map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"patch": json.RawMessage(jsonDiffPatch),
|
||||
key: json.RawMessage("[]"),
|
||||
},
|
||||
}
|
||||
*receivedMessage = receivedMessageMap
|
||||
}
|
||||
lastDataAsJsonString := string(lastContent)
|
||||
if string(dataAsJson) == lastDataAsJsonString {
|
||||
//Content didn't change, set message as null to avoid sending it to the browser
|
||||
//This case is usual when the middleware reconnects with Hasura and receives the data again
|
||||
*receivedMessage = nil
|
||||
} else {
|
||||
//Content was changed, creating json patch
|
||||
//If data is small (< minLengthToPatch) it's not worth creating the patch
|
||||
if lastDataAsJsonString != "" && len(string(dataAsJson)) > minLengthToPatch {
|
||||
diffPatch, e := jsonpatch.CreatePatch([]byte(lastDataAsJsonString), []byte(dataAsJson))
|
||||
if e != nil {
|
||||
log.Errorf("Error creating JSON patch:%v", e)
|
||||
return
|
||||
}
|
||||
jsonDiffPatch, err := json.Marshal(diffPatch)
|
||||
if err != nil {
|
||||
log.Errorf("Error marshaling patch array:", err)
|
||||
return
|
||||
}
|
||||
|
||||
//Store current result to be used to create json patch in the future
|
||||
if lastDataAsJsonString != "" || len(string(dataAsJsonString)) > minLengthToPatch {
|
||||
errWritingOutput := ioutil.WriteFile(filePath, []byte(dataAsJsonString), 0644)
|
||||
if errWritingOutput != nil {
|
||||
log.Errorf("Error on trying to write cache of json diff:", errWritingOutput)
|
||||
//Use patch if the length is {minShrinkToUsePatch}% smaller than the original msg
|
||||
if float64(len(string(jsonDiffPatch)))/float64(len(string(dataAsJson))) < minShrinkToUsePatch {
|
||||
//Modify receivedMessage to include the Patch and remove the previous data
|
||||
//The key of the original message is kept to avoid errors (Apollo-client expects to receive this prop)
|
||||
receivedMessageMap["payload"] = map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"patch": json.RawMessage(jsonDiffPatch),
|
||||
dataKey: json.RawMessage("[]"),
|
||||
},
|
||||
}
|
||||
*receivedMessage = receivedMessageMap
|
||||
}
|
||||
}
|
||||
|
||||
//Store current result to be used to create json patch in the future
|
||||
if lastDataAsJsonString != "" || len(string(dataAsJson)) > minLengthToPatch {
|
||||
errWritingOutput := ioutil.WriteFile(filePath, []byte(dataAsJson), 0644)
|
||||
if errWritingOutput != nil {
|
||||
log.Errorf("Error on trying to write cache of json diff:", errWritingOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -136,16 +136,21 @@ func InvalidateSessionTokenConnections(sessionTokenToInvalidate string) {
|
||||
for _, browserConnection := range BrowserConnections {
|
||||
if browserConnection.SessionToken == sessionTokenToInvalidate {
|
||||
if browserConnection.HasuraConnection != nil {
|
||||
//Close chan to force stop receiving new messages from the browser
|
||||
browserConnection.HasuraConnection.MsgReceivingActiveChan.Close()
|
||||
|
||||
// Wait until there are no active mutations
|
||||
for iterationCount := 0; iterationCount < 100; iterationCount++ {
|
||||
for iterationCount := 0; iterationCount < 20; iterationCount++ {
|
||||
activeMutationFound := false
|
||||
browserConnection.ActiveSubscriptionsMutex.RLock()
|
||||
for _, subscription := range browserConnection.ActiveSubscriptions {
|
||||
if subscription.Type == common.Mutation {
|
||||
activeMutationFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
browserConnection.ActiveSubscriptionsMutex.RUnlock()
|
||||
|
||||
if !activeMutationFound {
|
||||
break
|
||||
}
|
||||
|
@ -41,7 +41,6 @@ func BrowserConnectionReader(browserConnectionId string, ctx context.Context, c
|
||||
}
|
||||
|
||||
log.Tracef("received from browser: %v", v)
|
||||
//fmt.Println("received from browser: %v", v)
|
||||
|
||||
fromBrowserToHasuraChannel.Send(v)
|
||||
fromBrowserToHasuraConnectionEstablishingChannel.Send(v)
|
||||
|
@ -56,7 +56,7 @@ func StartRedisListener() {
|
||||
log.Debugf("Received invalidate request for sessionToken %v", sessionTokenToInvalidate)
|
||||
|
||||
//Not being used yet
|
||||
InvalidateSessionTokenConnections(sessionTokenToInvalidate.(string))
|
||||
go InvalidateSessionTokenConnections(sessionTokenToInvalidate.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"nhooyr.io/websocket/wsjson"
|
||||
)
|
||||
|
||||
func BrowserConnectionWriter(browserConnectionId string, ctx context.Context, c *websocket.Conn, fromHasuratoBrowserChannel *common.SafeChannel, wg *sync.WaitGroup) {
|
||||
func BrowserConnectionWriter(browserConnectionId string, ctx context.Context, c *websocket.Conn, fromHasuraToBrowserChannel *common.SafeChannel, wg *sync.WaitGroup) {
|
||||
log := log.WithField("_routine", "BrowserConnectionWriter").WithField("browserConnectionId", browserConnectionId)
|
||||
defer log.Debugf("finished")
|
||||
log.Debugf("starting")
|
||||
@ -21,7 +21,7 @@ RangeLoop:
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break RangeLoop
|
||||
case toBrowserMessage := <-fromHasuratoBrowserChannel.ReceiveChannel():
|
||||
case toBrowserMessage := <-fromHasuraToBrowserChannel.ReceiveChannel():
|
||||
{
|
||||
var toBrowserMessageAsMap = toBrowserMessage.(map[string]interface{})
|
||||
|
||||
|
@ -25,14 +25,16 @@ create table "meeting" (
|
||||
"bannerText" text,
|
||||
"bannerColor" varchar(50),
|
||||
"createdTime" bigint,
|
||||
"durationInSeconds" integer
|
||||
"durationInSeconds" integer,
|
||||
"endedAt" timestamp with time zone,
|
||||
"endedReasonCode" varchar(200),
|
||||
"endedBy" varchar(50)
|
||||
);
|
||||
ALTER TABLE "meeting" ADD COLUMN "createdAt" timestamp with time zone GENERATED ALWAYS AS (to_timestamp("createdTime"::double precision / 1000)) STORED;
|
||||
ALTER TABLE "meeting" ADD COLUMN "ended" boolean GENERATED ALWAYS AS ("endedAt" is not null) STORED;
|
||||
|
||||
create index "idx_meeting_extId" on "meeting"("extId");
|
||||
|
||||
create view "v_meeting" as select * from "meeting";
|
||||
|
||||
create table "meeting_breakout" (
|
||||
"meetingId" varchar(100) primary key references "meeting"("meetingId") ON DELETE CASCADE,
|
||||
"parentId" varchar(100),
|
||||
@ -428,6 +430,13 @@ AS SELECT "user"."userId",
|
||||
CASE WHEN "user"."joined" IS true AND "user"."expired" IS false AND "user"."loggedOut" IS false AND "user"."ejected" IS NOT TRUE THEN true ELSE false END "isOnline"
|
||||
FROM "user";
|
||||
|
||||
--This view will be used by Meteor to validate if the provided authToken is valid
|
||||
--It is temporary while Meteor is not removed
|
||||
create view "v_user_connection_auth" as
|
||||
select "meetingId", "userId", "authToken"
|
||||
from "v_user_current"
|
||||
where "isOnline" is true;
|
||||
|
||||
CREATE OR REPLACE VIEW "v_user_guest" AS
|
||||
SELECT u."meetingId", u."userId",
|
||||
u."guestStatus",
|
||||
@ -781,6 +790,14 @@ JOIN "user_reaction" ur ON u."userId" = ur."userId" AND "expiresAt" > current_ti
|
||||
GROUP BY u."meetingId", ur."userId";
|
||||
|
||||
|
||||
|
||||
create view "v_meeting" as
|
||||
select "meeting".*, "user_ended"."name" as "endedByUserName"
|
||||
from "meeting"
|
||||
left join "user" "user_ended" on "user_ended"."userId" = "meeting"."endedBy"
|
||||
;
|
||||
|
||||
|
||||
-- ===================== CHAT TABLES
|
||||
|
||||
|
||||
|
@ -18,6 +18,22 @@ sudo -u postgres psql -U postgres -d bbb_graphql -a -f bbb_schema.sql --set ON_E
|
||||
sudo -u postgres psql -c "drop database if exists hasura_app with (force)"
|
||||
sudo -u postgres psql -c "create database hasura_app"
|
||||
|
||||
echo "Creating frontend in bbb_graphql"
|
||||
DATABASE_FRONTEND_USER="bbb_frontend"
|
||||
FRONT_USER_EXISTS=$(sudo -u postgres psql -U postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname = '$DATABASE_FRONTEND_USER'")
|
||||
if [ "$FRONT_USER_EXISTS" = '1' ]
|
||||
then
|
||||
echo "User $DATABASE_FRONTEND_USER already exists"
|
||||
else
|
||||
sudo -u postgres psql -q -c "CREATE USER $DATABASE_FRONTEND_USER WITH PASSWORD '$DATABASE_FRONTEND_USER'"
|
||||
sudo -u postgres psql -q -c "GRANT CONNECT ON DATABASE bbb_graphql TO $DATABASE_FRONTEND_USER"
|
||||
sudo -u postgres psql -q -d bbb_graphql -c "REVOKE ALL ON ALL TABLES IN SCHEMA public FROM $DATABASE_FRONTEND_USER"
|
||||
sudo -u postgres psql -q -d bbb_graphql -c "GRANT USAGE ON SCHEMA public TO $DATABASE_FRONTEND_USER"
|
||||
echo "User $DATABASE_FRONTEND_USER created on database bbb_graphql"
|
||||
fi
|
||||
|
||||
sudo -u postgres psql -q -d bbb_graphql -c "GRANT SELECT ON v_user_connection_auth TO $DATABASE_FRONTEND_USER"
|
||||
|
||||
echo "Postgresql installed!"
|
||||
|
||||
|
||||
@ -39,7 +55,7 @@ systemctl restart nginx
|
||||
#chmod +x /usr/local/bin/hasura-graphql-engine
|
||||
|
||||
#Hasura 2.29+ requires Ubuntu 22
|
||||
git clone --branch v2.36.0 https://github.com/iMDT/hasura-graphql-engine.git
|
||||
git clone --branch v2.37.0 https://github.com/iMDT/hasura-graphql-engine.git
|
||||
cat hasura-graphql-engine/hasura-graphql.part-a* > hasura-graphql
|
||||
rm -rf hasura-graphql-engine/
|
||||
chmod +x hasura-graphql
|
||||
|
@ -280,6 +280,13 @@ type Mutation {
|
||||
): Boolean
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
presentationExport(
|
||||
presentationId: String!
|
||||
fileStateType: String!
|
||||
): Boolean
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
presentationRemove(
|
||||
presentationId: String!
|
||||
|
@ -245,6 +245,13 @@ actions:
|
||||
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
|
||||
permissions:
|
||||
- role: bbb_client
|
||||
- name: presentationExport
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
|
||||
permissions:
|
||||
- role: bbb_client
|
||||
comment: presentationExport
|
||||
- name: presentationRemove
|
||||
definition:
|
||||
kind: synchronous
|
||||
@ -382,7 +389,7 @@ actions:
|
||||
kind: synchronous
|
||||
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
|
||||
permissions:
|
||||
- role: pre_join_bbb_client
|
||||
- role: not_joined_bbb_client
|
||||
- role: bbb_client
|
||||
- name: userLeaveMeeting
|
||||
definition:
|
||||
|
@ -145,6 +145,11 @@ select_permissions:
|
||||
- customLogoUrl
|
||||
- disabledFeatures
|
||||
- durationInSeconds
|
||||
- ended
|
||||
- endedAt
|
||||
- endedBy
|
||||
- endedByUserName
|
||||
- endedReasonCode
|
||||
- extId
|
||||
- html5InstanceId
|
||||
- isBreakout
|
||||
@ -159,12 +164,17 @@ select_permissions:
|
||||
filter:
|
||||
meetingId:
|
||||
_eq: X-Hasura-MeetingId
|
||||
- role: pre_join_bbb_client
|
||||
- role: not_joined_bbb_client
|
||||
permission:
|
||||
columns:
|
||||
- bannerColor
|
||||
- bannerText
|
||||
- customLogoUrl
|
||||
- ended
|
||||
- endedAt
|
||||
- endedBy
|
||||
- endedByUserName
|
||||
- endedReasonCode
|
||||
- logoutUrl
|
||||
- meetingId
|
||||
- name
|
||||
|
@ -15,3 +15,11 @@ select_permissions:
|
||||
meetingId:
|
||||
_eq: X-Hasura-MeetingId
|
||||
comment: ""
|
||||
- role: not_joined_bbb_client
|
||||
permission:
|
||||
columns:
|
||||
- clientSettingsJson
|
||||
filter:
|
||||
meetingId:
|
||||
_eq: X-Hasura-MeetingId
|
||||
comment: ""
|
||||
|
@ -172,7 +172,7 @@ select_permissions:
|
||||
filter:
|
||||
userId:
|
||||
_eq: X-Hasura-UserId
|
||||
- role: pre_join_bbb_client
|
||||
- role: not_joined_bbb_client
|
||||
permission:
|
||||
columns:
|
||||
- authToken
|
||||
|
@ -34,7 +34,7 @@ select_permissions:
|
||||
- meetingId:
|
||||
_eq: X-Hasura-ModeratorInMeeting
|
||||
allow_aggregations: true
|
||||
- role: pre_join_bbb_client
|
||||
- role: not_joined_bbb_client
|
||||
permission:
|
||||
columns:
|
||||
- guestLobbyMessage
|
||||
|
@ -24,6 +24,22 @@ sudo -u postgres psql -c "alter database bbb_graphql set timezone to 'UTC'"
|
||||
echo "Creating tables in bbb_graphql"
|
||||
sudo -u postgres psql -U postgres -d bbb_graphql -q -f bbb_schema.sql --set ON_ERROR_STOP=on
|
||||
|
||||
echo "Creating frontend in bbb_graphql"
|
||||
DATABASE_FRONTEND_USER="bbb_frontend"
|
||||
FRONT_USER_EXISTS=$(sudo -u postgres psql -U postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname = '$DATABASE_FRONTEND_USER'")
|
||||
if [ "$FRONT_USER_EXISTS" = '1' ]
|
||||
then
|
||||
echo "User $DATABASE_FRONTEND_USER already exists"
|
||||
else
|
||||
sudo -u postgres psql -q -c "CREATE USER $DATABASE_FRONTEND_USER WITH PASSWORD '$DATABASE_FRONTEND_USER'"
|
||||
sudo -u postgres psql -q -c "GRANT CONNECT ON DATABASE bbb_graphql TO $DATABASE_FRONTEND_USER"
|
||||
sudo -u postgres psql -q -d bbb_graphql -c "REVOKE ALL ON ALL TABLES IN SCHEMA public FROM $DATABASE_FRONTEND_USER"
|
||||
sudo -u postgres psql -q -d bbb_graphql -c "GRANT USAGE ON SCHEMA public TO $DATABASE_FRONTEND_USER"
|
||||
echo "User $DATABASE_FRONTEND_USER created on database bbb_graphql"
|
||||
fi
|
||||
|
||||
sudo -u postgres psql -q -d bbb_graphql -c "GRANT SELECT ON v_user_connection_auth TO $DATABASE_FRONTEND_USER"
|
||||
|
||||
if [ "$hasura_status" = "active" ]; then
|
||||
echo "Starting Hasura"
|
||||
sudo systemctl start bbb-graphql-server
|
||||
|
@ -1 +0,0 @@
|
||||
import './methods';
|
@ -1,12 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import clearWhiteboard from './methods/clearWhiteboard';
|
||||
import sendAnnotations from './methods/sendAnnotations';
|
||||
import sendBulkAnnotations from './methods/sendBulkAnnotations';
|
||||
import deleteAnnotations from './methods/deleteAnnotations';
|
||||
|
||||
Meteor.methods({
|
||||
clearWhiteboard,
|
||||
sendAnnotations,
|
||||
sendBulkAnnotations,
|
||||
deleteAnnotations,
|
||||
});
|
@ -1,27 +0,0 @@
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function clearWhiteboard(whiteboardId) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'ClearWhiteboardPubMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(whiteboardId, String);
|
||||
|
||||
const payload = {
|
||||
whiteboardId,
|
||||
};
|
||||
|
||||
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method clearWhiteboard ${err.stack}`);
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function deleteAnnotations(annotations, whiteboardId) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'DeleteWhiteboardAnnotationsPubMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(whiteboardId, String);
|
||||
check(annotations, Array);
|
||||
|
||||
const payload = {
|
||||
whiteboardId,
|
||||
annotationsIds: annotations,
|
||||
};
|
||||
|
||||
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method deleteAnnotation ${err.stack}`);
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function sendAnnotationHelper(annotations, meetingId, requesterUserId) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'SendWhiteboardAnnotationsPubMsg';
|
||||
|
||||
try {
|
||||
check(annotations, Array);
|
||||
// TODO see if really necessary, don't know if it's possible
|
||||
// to have annotations from different pages
|
||||
// group annotations by same whiteboardId
|
||||
const groupedAnnotations = annotations.reduce((r, v, i, a, k = v.wbId) => ((r[k] || (r[k] = [])).push(v), r), {}) //groupBy wbId
|
||||
|
||||
Object.entries(groupedAnnotations).forEach(([_, whiteboardAnnotations]) => {
|
||||
const whiteboardId = whiteboardAnnotations[0].wbId;
|
||||
check(whiteboardId, String);
|
||||
|
||||
const payload = {
|
||||
whiteboardId,
|
||||
annotations: whiteboardAnnotations,
|
||||
html5InstanceId: parseInt(process.env.INSTANCE_ID, 10) || 1,
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method sendAnnotationHelper ${err.stack}`);
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import sendAnnotationHelper from './sendAnnotationHelper';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function sendAnnotations(annotations) {
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
sendAnnotationHelper(annotations, meetingId, requesterUserId);
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method sendAnnotation ${err.stack}`);
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import sendAnnotationHelper from './sendAnnotationHelper';
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function sendBulkAnnotations(payload) {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
try {
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
console.log("!!!!!!! sendBulkAnnotations!!!!:", payload)
|
||||
|
||||
sendAnnotationHelper(payload, meetingId, requesterUserId);
|
||||
//payload.forEach((annotation) => sendAnnotationHelper(annotation, meetingId, requesterUserId));
|
||||
return true;
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method sendBulkAnnotations ${err.stack}`);
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
const AudioCaptions = new Mongo.Collection('audio-captions');
|
||||
|
||||
if (Meteor.isServer) {
|
||||
AudioCaptions.createIndexAsync({ meetingId: 1 });
|
||||
}
|
||||
|
||||
export default AudioCaptions;
|
@ -1,4 +0,0 @@
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import handleTranscriptUpdated from '/imports/api/audio-captions/server/handlers/transcriptUpdated';
|
||||
|
||||
RedisPubSub.on('TranscriptUpdatedEvtMsg', handleTranscriptUpdated);
|
@ -1,12 +0,0 @@
|
||||
import setTranscript from '/imports/api/audio-captions/server/modifiers/setTranscript';
|
||||
|
||||
export default async function transcriptUpdated({ header, body }) {
|
||||
const { meetingId } = header;
|
||||
|
||||
const {
|
||||
transcriptId,
|
||||
transcript,
|
||||
} = body;
|
||||
|
||||
await setTranscript(meetingId, transcriptId, transcript);
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
import './eventHandlers';
|
||||
import './publishers';
|
@ -1,26 +0,0 @@
|
||||
import AudioCaptions from '/imports/api/audio-captions';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default async function clearAudioCaptions(meetingId) {
|
||||
if (meetingId) {
|
||||
try {
|
||||
const numberAffected = await AudioCaptions.removeAsync({ meetingId });
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.info(`Cleared AudioCaptions (${meetingId})`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Error on clearing audio captions (${meetingId}). ${err}`);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const numberAffected = await AudioCaptions.removeAsync({});
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.info('Cleared AudioCaptions (all)');
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Error on clearing audio captions (all). ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import AudioCaptions from '/imports/api/audio-captions';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default async function setTranscript(meetingId, transcriptId, transcript) {
|
||||
try {
|
||||
check(meetingId, String);
|
||||
check(transcriptId, String);
|
||||
check(transcript, String);
|
||||
|
||||
const selector = { meetingId };
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
transcriptId,
|
||||
transcript,
|
||||
},
|
||||
};
|
||||
|
||||
const numberAffected = await AudioCaptions.upsertAsync(selector, modifier);
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.debug(`Set transcriptId=${transcriptId} transcript=${transcript} meeting=${meetingId}`);
|
||||
} else {
|
||||
Logger.debug(`Upserted transcriptId=${transcriptId} transcript=${transcript} meeting=${meetingId}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Setting audio captions transcript to the collection: ${err}`);
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import AudioCaptions from '/imports/api/audio-captions';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
|
||||
|
||||
async function audioCaptions() {
|
||||
const tokenValidation = await AuthTokenValidation
|
||||
.findOneAsync({ connectionId: this.connection.id });
|
||||
|
||||
if (!tokenValidation || tokenValidation.validationStatus !== ValidationStates.VALIDATED) {
|
||||
Logger.warn(`Publishing AudioCaptions was requested by unauth connection ${this.connection.id}`);
|
||||
return AudioCaptions.find({ meetingId: '' });
|
||||
}
|
||||
|
||||
const { meetingId, userId } = tokenValidation;
|
||||
Logger.debug('Publishing AudioCaptions', { meetingId, requestedBy: userId });
|
||||
|
||||
return AudioCaptions.find({ meetingId });
|
||||
}
|
||||
|
||||
function publish(...args) {
|
||||
const boundAudioCaptions = audioCaptions.bind(this);
|
||||
return boundAudioCaptions(...args);
|
||||
}
|
||||
|
||||
Meteor.publish('audio-captions', publish);
|
@ -13,7 +13,6 @@ import clearVoiceUsers from '/imports/api/voice-users/server/modifiers/clearVoic
|
||||
import clearUserInfo from '/imports/api/users-infos/server/modifiers/clearUserInfo';
|
||||
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';
|
||||
import clearRecordMeeting from './clearRecordMeeting';
|
||||
@ -42,7 +41,6 @@ export default async function meetingHasEnded(meetingId) {
|
||||
clearVoiceUsers(meetingId),
|
||||
clearUserInfo(meetingId),
|
||||
clearTimer(meetingId),
|
||||
clearAudioCaptions(meetingId),
|
||||
clearLocalSettings(meetingId),
|
||||
clearMeetingTimeRemaining(meetingId),
|
||||
clearRecordMeeting(meetingId),
|
||||
|
@ -1 +0,0 @@
|
||||
import './methods';
|
@ -1,12 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import removePresentation from './methods/removePresentation';
|
||||
import setPresentation from './methods/setPresentation';
|
||||
import setPresentationDownloadable from './methods/setPresentationDownloadable';
|
||||
import exportPresentation from './methods/exportPresentation';
|
||||
|
||||
Meteor.methods({
|
||||
removePresentation,
|
||||
setPresentation,
|
||||
setPresentationDownloadable,
|
||||
exportPresentation,
|
||||
});
|
@ -1,29 +0,0 @@
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import { check } from 'meteor/check';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default async function exportPresentation(presentationId, fileStateType) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'MakePresentationDownloadReqMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(presentationId, String);
|
||||
|
||||
const payload = {
|
||||
presId: presentationId,
|
||||
allPages: true,
|
||||
fileStateType,
|
||||
pages: [],
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method exportPresentation ${err.stack}`);
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import { check } from 'meteor/check';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function removePresentation(presentationId, podId) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'RemovePresentationPubMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(presentationId, String);
|
||||
check(podId, String);
|
||||
|
||||
const payload = {
|
||||
presentationId,
|
||||
podId,
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method removePresentation ${err.stack}`);
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import { check } from 'meteor/check';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function setPresentation(presentationId, podId) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'SetCurrentPresentationPubMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(presentationId, String);
|
||||
check(podId, String);
|
||||
|
||||
const payload = {
|
||||
presentationId,
|
||||
podId,
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method setPresentation ${err.stack}`);
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import { check } from 'meteor/check';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function setPresentationDownloadable(presentationId, downloadable, fileStateType) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'SetPresentationDownloadablePubMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(downloadable, Match.Maybe(Boolean));
|
||||
check(presentationId, String);
|
||||
check(fileStateType, Match.Maybe(String));
|
||||
|
||||
const payload = {
|
||||
presentationId,
|
||||
podId: 'DEFAULT_PRESENTATION_POD',
|
||||
downloadable,
|
||||
fileStateType,
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method setPresentationDownloadable ${err.stack}`);
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
import './methods';
|
@ -1,6 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import switchSlide from './methods/switchSlide';
|
||||
|
||||
Meteor.methods({
|
||||
switchSlide,
|
||||
});
|
@ -1,30 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default async function switchSlide(slideNumber, podId, presentationId) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'SetCurrentPagePubMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(slideNumber, Number);
|
||||
check(podId, String);
|
||||
|
||||
const payload = {
|
||||
podId,
|
||||
presentationId,
|
||||
pageId: `${presentationId}/${slideNumber}`,
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method switchSlide ${err.stack}`);
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PresentationUploaderService from '/imports/ui/components/presentation/presentation-uploader/service';
|
||||
import ActionsDropdown from './component';
|
||||
import { layoutSelectInput, layoutDispatch, layoutSelect } from '../../layout/context';
|
||||
import { SMALL_VIEWPORT_BREAKPOINT, ACTIONS, PANELS } from '../../layout/enums';
|
||||
@ -12,6 +11,7 @@ import {
|
||||
import { SET_PRESENTER } from '/imports/ui/core/graphql/mutations/userMutations';
|
||||
import { TIMER_ACTIVATE, TIMER_DEACTIVATE } from '../../timer/mutations';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { PRESENTATION_SET_CURRENT } from '../../presentation/mutations';
|
||||
|
||||
const TIMER_CONFIG = Meteor.settings.public.timer;
|
||||
const MILLI_IN_MINUTE = 60000;
|
||||
@ -35,11 +35,16 @@ const ActionsDropdownContainer = (props) => {
|
||||
const [setPresenter] = useMutation(SET_PRESENTER);
|
||||
const [timerActivate] = useMutation(TIMER_ACTIVATE);
|
||||
const [timerDeactivate] = useMutation(TIMER_DEACTIVATE);
|
||||
const [presentationSetCurrent] = useMutation(PRESENTATION_SET_CURRENT);
|
||||
|
||||
const handleTakePresenter = () => {
|
||||
setPresenter({ variables: { userId: Auth.userID } });
|
||||
};
|
||||
|
||||
const setPresentation = (presentationId) => {
|
||||
presentationSetCurrent({ variables: { presentationId } });
|
||||
};
|
||||
|
||||
const activateTimer = () => {
|
||||
const stopwatch = true;
|
||||
const running = false;
|
||||
@ -71,7 +76,7 @@ const ActionsDropdownContainer = (props) => {
|
||||
presentations,
|
||||
isTimerFeatureEnabled: isTimerFeatureEnabled(),
|
||||
isDropdownOpen: Session.get('dropdownOpen'),
|
||||
setPresentation: PresentationUploaderService.setPresentation,
|
||||
setPresentation,
|
||||
isCameraAsContentEnabled: isCameraAsContentEnabled(),
|
||||
handleTakePresenter,
|
||||
activateTimer,
|
||||
|
@ -4,7 +4,7 @@ import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import { ActionsBarItemType, ActionsBarPosition } from 'bigbluebutton-html-plugin-sdk/dist/cjs/extensible-areas/actions-bar-item/enums';
|
||||
import Styled from './styles';
|
||||
import ActionsDropdown from './actions-dropdown/container';
|
||||
import AudioCaptionsButtonContainer from '/imports/ui/components/audio/captions/button/container';
|
||||
import AudioCaptionsButtonContainer from '/imports/ui/components/audio/audio-graphql/audio-captions/button/component';
|
||||
import CaptionsReaderMenuContainer from '/imports/ui/components/captions/reader-menu/container';
|
||||
import ScreenshareButtonContainer from '/imports/ui/components/actions-bar/screenshare/container';
|
||||
import ReactionsButtonContainer from './reactions-button/container';
|
||||
|
@ -3,7 +3,7 @@ import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Users from '/imports/api/users';
|
||||
import Meetings, { LayoutMeetings } from '/imports/api/meetings';
|
||||
import AudioCaptionsLiveContainer from '/imports/ui/components/audio/captions/live/container';
|
||||
import AudioCaptionsLiveContainer from '/imports/ui/components/audio/audio-graphql/audio-captions/live/component';
|
||||
import AudioCaptionsService from '/imports/ui/components/audio/captions/service';
|
||||
import { notify } from '/imports/ui/services/notification';
|
||||
import CaptionsContainer from '/imports/ui/components/captions/live/container';
|
||||
|
@ -1,259 +0,0 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Service from '/imports/ui/components/audio/captions/service';
|
||||
import SpeechService from '/imports/ui/components/audio/captions/speech/service';
|
||||
import ButtonEmoji from '/imports/ui/components/common/button/button-emoji/ButtonEmoji';
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import Styled from './styles';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { SET_SPEECH_LOCALE } from '/imports/ui/core/graphql/mutations/userMutations';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
start: {
|
||||
id: 'app.audio.captions.button.start',
|
||||
description: 'Start audio captions',
|
||||
},
|
||||
stop: {
|
||||
id: 'app.audio.captions.button.stop',
|
||||
description: 'Stop audio captions',
|
||||
},
|
||||
transcriptionSettings: {
|
||||
id: 'app.audio.captions.button.transcriptionSettings',
|
||||
description: 'Audio captions settings modal',
|
||||
},
|
||||
transcription: {
|
||||
id: 'app.audio.captions.button.transcription',
|
||||
description: 'Audio speech transcription label',
|
||||
},
|
||||
transcriptionOn: {
|
||||
id: 'app.switch.onLabel',
|
||||
},
|
||||
transcriptionOff: {
|
||||
id: 'app.switch.offLabel',
|
||||
},
|
||||
language: {
|
||||
id: 'app.audio.captions.button.language',
|
||||
description: 'Audio speech recognition language label',
|
||||
},
|
||||
'de-DE': {
|
||||
id: 'app.audio.captions.select.de-DE',
|
||||
description: 'Audio speech recognition german language',
|
||||
},
|
||||
'en-US': {
|
||||
id: 'app.audio.captions.select.en-US',
|
||||
description: 'Audio speech recognition english language',
|
||||
},
|
||||
'es-ES': {
|
||||
id: 'app.audio.captions.select.es-ES',
|
||||
description: 'Audio speech recognition spanish language',
|
||||
},
|
||||
'fr-FR': {
|
||||
id: 'app.audio.captions.select.fr-FR',
|
||||
description: 'Audio speech recognition french language',
|
||||
},
|
||||
'hi-ID': {
|
||||
id: 'app.audio.captions.select.hi-ID',
|
||||
description: 'Audio speech recognition indian language',
|
||||
},
|
||||
'it-IT': {
|
||||
id: 'app.audio.captions.select.it-IT',
|
||||
description: 'Audio speech recognition italian language',
|
||||
},
|
||||
'ja-JP': {
|
||||
id: 'app.audio.captions.select.ja-JP',
|
||||
description: 'Audio speech recognition japanese language',
|
||||
},
|
||||
'pt-BR': {
|
||||
id: 'app.audio.captions.select.pt-BR',
|
||||
description: 'Audio speech recognition portuguese language',
|
||||
},
|
||||
'ru-RU': {
|
||||
id: 'app.audio.captions.select.ru-RU',
|
||||
description: 'Audio speech recognition russian language',
|
||||
},
|
||||
'zh-CN': {
|
||||
id: 'app.audio.captions.select.zh-CN',
|
||||
description: 'Audio speech recognition chinese language',
|
||||
},
|
||||
});
|
||||
|
||||
const DEFAULT_LOCALE = 'en-US';
|
||||
const DISABLED = '';
|
||||
|
||||
const CaptionsButton = ({
|
||||
intl,
|
||||
active,
|
||||
isRTL,
|
||||
enabled,
|
||||
currentSpeechLocale,
|
||||
availableVoices,
|
||||
isSupported,
|
||||
isVoiceUser,
|
||||
}) => {
|
||||
const isTranscriptionDisabled = () => (
|
||||
currentSpeechLocale === DISABLED
|
||||
);
|
||||
|
||||
const [setSpeechLocale] = useMutation(SET_SPEECH_LOCALE);
|
||||
|
||||
const setUserSpeechLocale = (speechLocale, provider) => {
|
||||
setSpeechLocale({
|
||||
variables: {
|
||||
locale: speechLocale,
|
||||
provider,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const fallbackLocale = availableVoices.includes(navigator.language)
|
||||
? navigator.language : DEFAULT_LOCALE;
|
||||
|
||||
const getSelectedLocaleValue = (isTranscriptionDisabled() ? fallbackLocale : currentSpeechLocale);
|
||||
|
||||
const selectedLocale = useRef(getSelectedLocaleValue);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTranscriptionDisabled()) selectedLocale.current = getSelectedLocaleValue;
|
||||
}, [currentSpeechLocale]);
|
||||
|
||||
if (!enabled) return null;
|
||||
|
||||
const shouldRenderChevron = isSupported && isVoiceUser;
|
||||
|
||||
const getAvailableLocales = () => {
|
||||
let indexToInsertSeparator = -1;
|
||||
const availableVoicesObjectToMenu = availableVoices.map((availableVoice, index) => {
|
||||
if (availableVoice === availableVoices[0]) {
|
||||
indexToInsertSeparator = index;
|
||||
}
|
||||
return (
|
||||
{
|
||||
icon: '',
|
||||
label: intl.formatMessage(intlMessages[availableVoice]),
|
||||
key: availableVoice,
|
||||
iconRight: selectedLocale.current === availableVoice ? 'check' : null,
|
||||
customStyles: (selectedLocale.current === availableVoice) && Styled.SelectedLabel,
|
||||
disabled: isTranscriptionDisabled(),
|
||||
onClick: () => {
|
||||
selectedLocale.current = availableVoice;
|
||||
SpeechService.setSpeechLocale(selectedLocale.current, setUserSpeechLocale);
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
if (indexToInsertSeparator >= 0) {
|
||||
availableVoicesObjectToMenu.splice(indexToInsertSeparator, 0, {
|
||||
key: 'separator-01',
|
||||
isSeparator: true,
|
||||
});
|
||||
}
|
||||
return [
|
||||
...availableVoicesObjectToMenu,
|
||||
];
|
||||
};
|
||||
|
||||
const toggleTranscription = () => {
|
||||
SpeechService.setSpeechLocale(
|
||||
isTranscriptionDisabled() ? selectedLocale.current : DISABLED, setUserSpeechLocale,
|
||||
);
|
||||
};
|
||||
|
||||
const getAvailableLocalesList = () => (
|
||||
[{
|
||||
key: 'availableLocalesList',
|
||||
label: intl.formatMessage(intlMessages.language),
|
||||
customStyles: Styled.TitleLabel,
|
||||
disabled: true,
|
||||
},
|
||||
...getAvailableLocales(),
|
||||
{
|
||||
key: 'divider',
|
||||
label: intl.formatMessage(intlMessages.transcription),
|
||||
customStyles: Styled.TitleLabel,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: 'separator-02',
|
||||
isSeparator: true,
|
||||
},
|
||||
{
|
||||
key: 'transcriptionStatus',
|
||||
label: intl.formatMessage(
|
||||
isTranscriptionDisabled()
|
||||
? intlMessages.transcriptionOn
|
||||
: intlMessages.transcriptionOff,
|
||||
),
|
||||
customStyles: isTranscriptionDisabled()
|
||||
? Styled.EnableTrascription : Styled.DisableTrascription,
|
||||
disabled: false,
|
||||
onClick: toggleTranscription,
|
||||
}]
|
||||
);
|
||||
|
||||
const onToggleClick = (e) => {
|
||||
e.stopPropagation();
|
||||
Service.setAudioCaptions(!active);
|
||||
};
|
||||
|
||||
const startStopCaptionsButton = (
|
||||
<Styled.ClosedCaptionToggleButton
|
||||
icon={active ? 'closed_caption' : 'closed_caption_stop'}
|
||||
label={intl.formatMessage(active ? intlMessages.stop : intlMessages.start)}
|
||||
color={active ? 'primary' : 'default'}
|
||||
ghost={!active}
|
||||
hideLabel
|
||||
circle
|
||||
size="lg"
|
||||
onClick={onToggleClick}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
shouldRenderChevron
|
||||
? (
|
||||
<Styled.SpanButtonWrapper>
|
||||
<BBBMenu
|
||||
trigger={(
|
||||
<>
|
||||
{ startStopCaptionsButton }
|
||||
<ButtonEmoji
|
||||
emoji="device_list_selector"
|
||||
hideLabel
|
||||
label={intl.formatMessage(intlMessages.transcriptionSettings)}
|
||||
tabIndex={0}
|
||||
rotate
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
actions={getAvailableLocalesList()}
|
||||
opts={{
|
||||
id: 'default-dropdown-menu',
|
||||
keepMounted: true,
|
||||
transitionDuration: 0,
|
||||
elevation: 3,
|
||||
getcontentanchorel: null,
|
||||
fullwidth: 'true',
|
||||
anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
|
||||
transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
|
||||
}}
|
||||
/>
|
||||
</Styled.SpanButtonWrapper>
|
||||
) : startStopCaptionsButton
|
||||
);
|
||||
};
|
||||
|
||||
CaptionsButton.propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
active: PropTypes.bool.isRequired,
|
||||
isRTL: PropTypes.bool.isRequired,
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
currentSpeechLocale: PropTypes.string.isRequired,
|
||||
availableVoices: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
isSupported: PropTypes.bool.isRequired,
|
||||
isVoiceUser: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CaptionsButton);
|
@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Service from '/imports/ui/components/audio/captions/service';
|
||||
import Button from './component';
|
||||
import SpeechService from '/imports/ui/components/audio/captions/speech/service';
|
||||
import AudioService from '/imports/ui/components/audio/service';
|
||||
import AudioCaptionsButtonContainer from '../../audio-graphql/audio-captions/button/component';
|
||||
|
||||
const Container = (props) => <Button {...props} />;
|
||||
|
||||
withTracker(() => {
|
||||
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
|
||||
const availableVoices = SpeechService.getSpeechVoices();
|
||||
const currentSpeechLocale = SpeechService.getSpeechLocale();
|
||||
const isSupported = availableVoices.length > 0;
|
||||
const isVoiceUser = AudioService.isVoiceUser();
|
||||
return {
|
||||
isRTL,
|
||||
enabled: Service.hasAudioCaptions(),
|
||||
active: Service.getAudioCaptions(),
|
||||
currentSpeechLocale,
|
||||
availableVoices,
|
||||
isSupported,
|
||||
isVoiceUser,
|
||||
};
|
||||
})(Container);
|
||||
|
||||
export default AudioCaptionsButtonContainer;
|
@ -1,61 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import Toggle from '/imports/ui/components/common/switch/component';
|
||||
import {
|
||||
colorWhite,
|
||||
colorPrimary,
|
||||
colorOffWhite,
|
||||
colorDangerDark,
|
||||
colorSuccess,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
|
||||
const ClosedCaptionToggleButton = styled(Button)`
|
||||
${({ ghost }) => ghost && `
|
||||
span {
|
||||
box-shadow: none;
|
||||
background-color: transparent !important;
|
||||
border-color: ${colorWhite} !important;
|
||||
}
|
||||
i {
|
||||
margin-top: .4rem;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const SpanButtonWrapper = styled.span`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const TranscriptionToggle = styled(Toggle)`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
padding-left: 1em;
|
||||
`;
|
||||
|
||||
const TitleLabel = {
|
||||
fontWeight: 'bold',
|
||||
opacity: 1,
|
||||
};
|
||||
|
||||
const EnableTrascription = {
|
||||
color: colorSuccess,
|
||||
};
|
||||
|
||||
const DisableTrascription = {
|
||||
color: colorDangerDark,
|
||||
};
|
||||
|
||||
const SelectedLabel = {
|
||||
color: colorPrimary,
|
||||
backgroundColor: colorOffWhite,
|
||||
};
|
||||
|
||||
export default {
|
||||
ClosedCaptionToggleButton,
|
||||
SpanButtonWrapper,
|
||||
TranscriptionToggle,
|
||||
TitleLabel,
|
||||
EnableTrascription,
|
||||
DisableTrascription,
|
||||
SelectedLabel,
|
||||
};
|
@ -1,104 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import UserContainer from './user/container';
|
||||
|
||||
const CAPTIONS_CONFIG = Meteor.settings.public.captions;
|
||||
|
||||
class LiveCaptions extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { clear: true };
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { clear } = this.state;
|
||||
|
||||
if (clear) {
|
||||
const { transcript } = this.props;
|
||||
if (prevProps.transcript !== transcript) {
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({ clear: false });
|
||||
}
|
||||
} else {
|
||||
this.resetTimer();
|
||||
this.timer = setTimeout(() => this.setState({ clear: true }), CAPTIONS_CONFIG.time);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.resetTimer();
|
||||
}
|
||||
|
||||
resetTimer() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
transcript,
|
||||
transcriptId,
|
||||
} = this.props;
|
||||
|
||||
const { clear } = this.state;
|
||||
|
||||
const hasContent = transcript.length > 0 && !clear;
|
||||
|
||||
const wrapperStyles = {
|
||||
display: 'flex',
|
||||
};
|
||||
|
||||
const captionStyles = {
|
||||
whiteSpace: 'pre-line',
|
||||
wordWrap: 'break-word',
|
||||
fontFamily: 'Verdana, Arial, Helvetica, sans-serif',
|
||||
fontSize: '1.5rem',
|
||||
background: '#000000a0',
|
||||
color: 'white',
|
||||
padding: hasContent ? '.5rem' : undefined,
|
||||
};
|
||||
|
||||
const visuallyHidden = {
|
||||
position: 'absolute',
|
||||
overflow: 'hidden',
|
||||
clip: 'rect(0 0 0 0)',
|
||||
height: '1px',
|
||||
width: '1px',
|
||||
margin: '-1px',
|
||||
padding: '0',
|
||||
border: '0',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={wrapperStyles}>
|
||||
{clear ? null : (
|
||||
<UserContainer
|
||||
background="#000000a0"
|
||||
transcriptId={transcriptId}
|
||||
/>
|
||||
)}
|
||||
<div style={captionStyles}>
|
||||
{clear ? '' : transcript}
|
||||
</div>
|
||||
<div
|
||||
style={visuallyHidden}
|
||||
aria-atomic
|
||||
aria-live="polite"
|
||||
>
|
||||
{clear ? '' : transcript}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LiveCaptions.propTypes = {
|
||||
transcript: PropTypes.string.isRequired,
|
||||
transcriptId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default LiveCaptions;
|
@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Service from '/imports/ui/components/audio/captions/service';
|
||||
import LiveCaptions from './component';
|
||||
import AudioCaptionsLiveContainer from '../../audio-graphql/audio-captions/live/component';
|
||||
|
||||
const Container = (props) => <LiveCaptions {...props} />;
|
||||
|
||||
withTracker(() => {
|
||||
const {
|
||||
transcriptId,
|
||||
transcript,
|
||||
} = Service.getAudioCaptionsData();
|
||||
|
||||
return {
|
||||
transcript,
|
||||
transcriptId,
|
||||
};
|
||||
})(Container);
|
||||
|
||||
export default AudioCaptionsLiveContainer;
|
@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import UserAvatar from '/imports/ui/components/user-avatar/component';
|
||||
|
||||
const User = ({
|
||||
avatar,
|
||||
background,
|
||||
color,
|
||||
moderator,
|
||||
name,
|
||||
}) => (
|
||||
<div
|
||||
style={{
|
||||
background,
|
||||
minHeight: '3.25rem',
|
||||
padding: '.5rem',
|
||||
textTransform: 'capitalize',
|
||||
width: '3.25rem',
|
||||
}}
|
||||
>
|
||||
<UserAvatar
|
||||
avatar={avatar}
|
||||
color={color}
|
||||
moderator={moderator}
|
||||
>
|
||||
{name.slice(0, 2)}
|
||||
</UserAvatar>
|
||||
</div>
|
||||
);
|
||||
|
||||
User.propTypes = {
|
||||
avatar: PropTypes.string.isRequired,
|
||||
background: PropTypes.string.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
moderator: PropTypes.bool.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default User;
|
@ -1,44 +0,0 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Users from '/imports/api/users';
|
||||
import User from './component';
|
||||
|
||||
const MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
|
||||
const Container = (props) => <User {...props} />;
|
||||
|
||||
const getUser = (userId) => {
|
||||
const user = Users.findOne(
|
||||
{ userId },
|
||||
{
|
||||
fields: {
|
||||
avatar: 1,
|
||||
color: 1,
|
||||
role: 1,
|
||||
name: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (user) {
|
||||
return {
|
||||
avatar: user.avatar,
|
||||
color: user.color,
|
||||
moderator: user.role === MODERATOR,
|
||||
name: user.name,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
avatar: '',
|
||||
color: '',
|
||||
moderator: false,
|
||||
name: '',
|
||||
};
|
||||
};
|
||||
|
||||
export default withTracker(({ transcriptId }) => {
|
||||
const userId = transcriptId.split('-')[0];
|
||||
|
||||
return getUser(userId);
|
||||
})(Container);
|
@ -1,38 +1,8 @@
|
||||
import AudioCaptions from '/imports/api/audio-captions';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
|
||||
const getAudioCaptionsData = () => {
|
||||
const audioCaptions = AudioCaptions.findOne({ meetingId: Auth.meetingID });
|
||||
|
||||
if (audioCaptions) {
|
||||
return {
|
||||
transcriptId: audioCaptions.transcriptId,
|
||||
transcript: audioCaptions.transcript,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
transcriptId: '',
|
||||
transcript: '',
|
||||
};
|
||||
};
|
||||
|
||||
const getAudioCaptions = () => Session.get('audioCaptions') || false;
|
||||
|
||||
const setAudioCaptions = (value) => Session.set('audioCaptions', value);
|
||||
|
||||
const hasAudioCaptions = () => {
|
||||
const audioCaptions = AudioCaptions.findOne(
|
||||
{ meetingId: Auth.meetingID },
|
||||
{ fields: {} },
|
||||
);
|
||||
|
||||
return !!audioCaptions;
|
||||
};
|
||||
|
||||
export default {
|
||||
getAudioCaptionsData,
|
||||
getAudioCaptions,
|
||||
setAudioCaptions,
|
||||
hasAudioCaptions,
|
||||
};
|
||||
|
@ -46,9 +46,6 @@ function assertAsMetadata(metadata: unknown): asserts metadata is Metadata {
|
||||
if (!Array.isArray((metadata as Metadata).answers)) {
|
||||
throw new Error('metadata.answers is not an array');
|
||||
}
|
||||
if ((metadata as Metadata).answers.length === 0) {
|
||||
throw new Error('metadata.answers is empty');
|
||||
}
|
||||
}
|
||||
|
||||
const ChatPollContent: React.FC<ChatPollContentProps> = ({
|
||||
|
@ -5,6 +5,8 @@ import audioManager from '/imports/ui/services/audio-manager';
|
||||
import { Session } from 'meteor/session';
|
||||
import { useReactiveVar, useMutation } from '@apollo/client';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import { ExternalVideoVolumeCommandsEnum } from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-commands/external-video/volume/enums';
|
||||
import { SetExternalVideoVolumeCommandArguments } from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-commands/external-video/volume/types';
|
||||
import { OnProgressProps } from 'react-player/base';
|
||||
|
||||
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
||||
@ -217,6 +219,14 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
|
||||
const handleExternalVideoVolumeSet = ((
|
||||
event: CustomEvent<SetExternalVideoVolumeCommandArguments>,
|
||||
) => setVolume(event.detail.volume)) as EventListener;
|
||||
window.addEventListener(ExternalVideoVolumeCommandsEnum.SET, handleExternalVideoVolumeSet);
|
||||
return () => {
|
||||
window.addEventListener(ExternalVideoVolumeCommandsEnum.SET, handleExternalVideoVolumeSet);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -4,8 +4,6 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||
import {
|
||||
IsBreakoutSubscriptionData,
|
||||
MEETING_ISBREAKOUT_SUBSCRIPTION,
|
||||
TALKING_INDICATOR_SUBSCRIPTION,
|
||||
TalkingIndicatorSubscriptionData,
|
||||
} from './queries';
|
||||
import { UserVoice } from '/imports/ui/Types/userVoice';
|
||||
import { uniqueId } from '/imports/utils/string-utils';
|
||||
@ -13,6 +11,7 @@ import Styled from './styles';
|
||||
import { User } from '/imports/ui/Types/user';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import { muteUser } from './service';
|
||||
import useTalkingIndicator from '/imports/ui/core/hooks/useTalkingIndicator';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
@ -178,15 +177,8 @@ const TalkingIndicatorContainer: React.FC = (() => {
|
||||
const {
|
||||
data: talkingIndicatorData,
|
||||
loading: talkingIndicatorLoading,
|
||||
error: talkingIndicatorError,
|
||||
} = useSubscription<TalkingIndicatorSubscriptionData>(
|
||||
TALKING_INDICATOR_SUBSCRIPTION,
|
||||
{
|
||||
variables: {
|
||||
limit: TALKING_INDICATORS_MAX,
|
||||
},
|
||||
},
|
||||
);
|
||||
errors: talkingIndicatorError,
|
||||
} = useTalkingIndicator((c) => c);
|
||||
|
||||
const {
|
||||
data: isBreakoutData,
|
||||
@ -205,7 +197,7 @@ const TalkingIndicatorContainer: React.FC = (() => {
|
||||
);
|
||||
}
|
||||
|
||||
const talkingUsers = talkingIndicatorData?.user_voice ?? [];
|
||||
const talkingUsers = talkingIndicatorData ?? [];
|
||||
const isBreakout = isBreakoutData?.meeting[0]?.isBreakout ?? false;
|
||||
|
||||
return (
|
||||
|
@ -1,43 +1,14 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { UserVoice } from '/imports/ui/Types/userVoice';
|
||||
|
||||
interface IsBreakoutData {
|
||||
meetingId: string;
|
||||
isBreakout: boolean;
|
||||
}
|
||||
|
||||
export interface TalkingIndicatorSubscriptionData {
|
||||
// eslint-disable-next-line camelcase
|
||||
user_voice: Array<Partial<UserVoice>>;
|
||||
}
|
||||
|
||||
export interface IsBreakoutSubscriptionData {
|
||||
meeting: Array<IsBreakoutData>;
|
||||
}
|
||||
|
||||
export const TALKING_INDICATOR_SUBSCRIPTION = gql`
|
||||
subscription TalkingIndicatorSubscription($limit: Int!) {
|
||||
user_voice(
|
||||
where: { showTalkingIndicator: { _eq: true } }
|
||||
order_by: [{ startTime: desc_nulls_last }, { endTime: desc_nulls_last }]
|
||||
limit: $limit
|
||||
) {
|
||||
callerName
|
||||
spoke
|
||||
talking
|
||||
floor
|
||||
startTime
|
||||
muted
|
||||
userId
|
||||
user {
|
||||
color
|
||||
name
|
||||
speechLocale
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// TODO: rework when useMeeting hook be avaible
|
||||
export const MEETING_ISBREAKOUT_SUBSCRIPTION = gql`
|
||||
subscription getIsBreakout {
|
||||
|
@ -12,6 +12,7 @@ import { SMALL_VIEWPORT_BREAKPOINT } from '../../layout/enums';
|
||||
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
|
||||
import { USER_LEAVE_MEETING } from '/imports/ui/core/graphql/mutations/userMutations';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
||||
|
||||
const { isIphone } = deviceInfo;
|
||||
const { isSafari, isValidSafariVersion } = browserInfo;
|
||||
@ -30,11 +31,22 @@ const OptionsDropdownContainer = (props) => {
|
||||
];
|
||||
}
|
||||
|
||||
const {
|
||||
data: currentMeeting,
|
||||
} = useMeeting((m) => {
|
||||
return {
|
||||
componentsFlags: m.componentsFlags,
|
||||
};
|
||||
});
|
||||
|
||||
const componentsFlags = currentMeeting?.componentsFlags;
|
||||
const audioCaptionsEnabled = componentsFlags?.hasCaption;
|
||||
|
||||
const [userLeaveMeeting] = useMutation(USER_LEAVE_MEETING);
|
||||
|
||||
return (
|
||||
<OptionsDropdown {...{
|
||||
isMobile, isRTL, optionsDropdownItems, userLeaveMeeting, ...props,
|
||||
isMobile, isRTL, optionsDropdownItems, userLeaveMeeting, audioCaptionsEnabled, ...props,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@ -44,7 +56,6 @@ export default withTracker((props) => {
|
||||
const handleToggleFullscreen = () => FullscreenService.toggleFullScreen();
|
||||
return {
|
||||
amIModerator: props.amIModerator,
|
||||
audioCaptionsEnabled: audioCaptionsService.hasAudioCaptions(),
|
||||
audioCaptionsActive: audioCaptionsService.getAudioCaptions(),
|
||||
audioCaptionsSet: (value) => audioCaptionsService.setAudioCaptions(value),
|
||||
isMobile: deviceInfo.isMobile,
|
||||
|
@ -51,6 +51,8 @@ class NotesDropdown extends PureComponent {
|
||||
intl,
|
||||
amIPresenter,
|
||||
presentations,
|
||||
setPresentation,
|
||||
removePresentation,
|
||||
stopExternalVideoShare,
|
||||
} = this.props;
|
||||
|
||||
@ -72,7 +74,7 @@ class NotesDropdown extends PureComponent {
|
||||
onClick: () => {
|
||||
this.setConverterButtonDisabled(true);
|
||||
setTimeout(() => this.setConverterButtonDisabled(false), DEBOUNCE_TIMEOUT);
|
||||
return Service.convertAndUpload(presentations);
|
||||
return Service.convertAndUpload(presentations, setPresentation, removePresentation);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
PROCESSED_PRESENTATIONS_SUBSCRIPTION,
|
||||
} from '/imports/ui/components/whiteboard/queries';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import { PRESENTATION_SET_CURRENT, PRESENTATION_REMOVE } from '../../presentation/mutations';
|
||||
import { EXTERNAL_VIDEO_STOP } from '../../external-video-player/mutations';
|
||||
|
||||
const NotesDropdownContainer = ({ ...props }) => {
|
||||
@ -18,8 +19,18 @@ const NotesDropdownContainer = ({ ...props }) => {
|
||||
const { data: presentationData } = useSubscription(PROCESSED_PRESENTATIONS_SUBSCRIPTION);
|
||||
const presentations = presentationData?.pres_presentation || [];
|
||||
|
||||
const [presentationSetCurrent] = useMutation(PRESENTATION_SET_CURRENT);
|
||||
const [presentationRemove] = useMutation(PRESENTATION_REMOVE);
|
||||
const [stopExternalVideoShare] = useMutation(EXTERNAL_VIDEO_STOP);
|
||||
|
||||
const setPresentation = (presentationId) => {
|
||||
presentationSetCurrent({ variables: { presentationId } });
|
||||
};
|
||||
|
||||
const removePresentation = (presentationId) => {
|
||||
presentationRemove({ variables: { presentationId } });
|
||||
};
|
||||
|
||||
return (
|
||||
<NotesDropdown
|
||||
{
|
||||
@ -27,6 +38,8 @@ const NotesDropdownContainer = ({ ...props }) => {
|
||||
amIPresenter,
|
||||
isRTL,
|
||||
presentations,
|
||||
setPresentation,
|
||||
removePresentation,
|
||||
stopExternalVideoShare,
|
||||
...props,
|
||||
}
|
||||
|
@ -7,8 +7,7 @@ import { uniqueId } from '/imports/utils/string-utils';
|
||||
|
||||
const PADS_CONFIG = Meteor.settings.public.pads;
|
||||
|
||||
async function convertAndUpload(presentations) {
|
||||
|
||||
async function convertAndUpload(presentations, setPresentation, removePresentation) {
|
||||
let filename = 'Shared_Notes';
|
||||
const duplicates = presentations.filter((pres) => pres.filename?.startsWith(filename) || pres.name?.startsWith(filename)).length;
|
||||
|
||||
@ -53,7 +52,9 @@ async function convertAndUpload(presentations) {
|
||||
onUpload: () => { },
|
||||
onProgress: () => { },
|
||||
onDone: () => { },
|
||||
});
|
||||
},
|
||||
setPresentation,
|
||||
removePresentation);
|
||||
}
|
||||
|
||||
export default {
|
||||
|
@ -2,13 +2,17 @@ import React, { useEffect } from 'react';
|
||||
import { useMutation, useSubscription } from '@apollo/client';
|
||||
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
|
||||
|
||||
import { createChannelIdentifier } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/hooks';
|
||||
import { createChannelIdentifier } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/utils';
|
||||
import {
|
||||
DataChannelArguments,
|
||||
DispatcherFunction, ObjectTo, ToRole, ToUserId,
|
||||
} from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types';
|
||||
import { DataChannelHooks } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/enums';
|
||||
import { HookEvents } from 'bigbluebutton-html-plugin-sdk/dist/cjs/core/enum';
|
||||
import { HookEventWrapper, UpdatedEventDetails } from 'bigbluebutton-html-plugin-sdk/dist/cjs/core/types';
|
||||
|
||||
import { PLUGIN_DATA_CHANNEL_DISPATCH_QUERY, PLUGIN_DATA_CHANNEL_FETCH_QUERY } from '../queries';
|
||||
import PLUGIN_DATA_CHANNEL_FETCH_QUERY from '../queries';
|
||||
import { PLUGIN_DATA_CHANNEL_DELETE_MUTATION, PLUGIN_DATA_CHANNEL_DISPATCH_MUTATION, PLUGIN_DATA_CHANNEL_RESET_MUTATION } from '../mutation';
|
||||
|
||||
export interface DataChannelItemManagerProps {
|
||||
pluginName: string;
|
||||
@ -35,7 +39,9 @@ export const DataChannelItemManager: React.ElementType<DataChannelItemManagerPro
|
||||
const pluginIdentifier = createChannelIdentifier(channelName, pluginName);
|
||||
|
||||
const dataChannelIdentifier = createChannelIdentifier(channelName, pluginName);
|
||||
const [dispatchPluginDataChannelMessage] = useMutation(PLUGIN_DATA_CHANNEL_DISPATCH_QUERY);
|
||||
const [dispatchPluginDataChannelMessage] = useMutation(PLUGIN_DATA_CHANNEL_DISPATCH_MUTATION);
|
||||
const [deletePluginDataChannelMessage] = useMutation(PLUGIN_DATA_CHANNEL_DELETE_MUTATION);
|
||||
const [resetPluginDataChannelMessage] = useMutation(PLUGIN_DATA_CHANNEL_RESET_MUTATION);
|
||||
|
||||
const data = useSubscription(PLUGIN_DATA_CHANNEL_FETCH_QUERY, {
|
||||
variables: {
|
||||
@ -81,12 +87,44 @@ export const DataChannelItemManager: React.ElementType<DataChannelItemManagerPro
|
||||
pluginApi.mapOfDispatchers[pluginIdentifier] = useDataChannelHandlerFunction;
|
||||
window.dispatchEvent(new Event(`${pluginIdentifier}::dispatcherFunction`));
|
||||
|
||||
const deleteOrResetHandler: EventListener = (
|
||||
(event: HookEventWrapper<void>) => {
|
||||
if (event.detail.hook === DataChannelHooks.DATA_CHANNEL_DELETE) {
|
||||
const eventDetails = event.detail as UpdatedEventDetails<string>;
|
||||
const hookArguments = eventDetails?.hookArguments as DataChannelArguments | undefined;
|
||||
deletePluginDataChannelMessage({
|
||||
variables: {
|
||||
pluginName: hookArguments?.pluginName,
|
||||
dataChannel: hookArguments?.channelName,
|
||||
messageId: eventDetails.data,
|
||||
},
|
||||
});
|
||||
} else if (event.detail.hook === DataChannelHooks.DATA_CHANNEL_RESET) {
|
||||
const eventDetails = event.detail as UpdatedEventDetails<void>;
|
||||
const hookArguments = eventDetails?.hookArguments as DataChannelArguments | undefined;
|
||||
resetPluginDataChannelMessage({
|
||||
variables: {
|
||||
pluginName: hookArguments?.pluginName,
|
||||
dataChannel: hookArguments?.channelName,
|
||||
},
|
||||
});
|
||||
}
|
||||
}) as EventListener;
|
||||
|
||||
useEffect(() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(dataChannelIdentifier, {
|
||||
detail: { hook: DataChannelHooks.DATA_CHANNEL, data },
|
||||
detail: { hook: DataChannelHooks.DATA_CHANNEL_BUILDER, data },
|
||||
}),
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener(HookEvents.UPDATED, deleteOrResetHandler);
|
||||
return () => {
|
||||
window.removeEventListener(HookEvents.UPDATED, deleteOrResetHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { createChannelIdentifier } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/hooks';
|
||||
import { createChannelIdentifier } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/utils';
|
||||
import { DataChannelArguments } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types';
|
||||
import {
|
||||
HookEventWrapper, UnsubscribedEventDetails, SubscribedEventDetails,
|
||||
@ -42,7 +42,7 @@ const PluginDataChannelManager: React.ElementType<PluginDataChannelManagerProps>
|
||||
useEffect(() => {
|
||||
const subscribeHandler: EventListener = (
|
||||
(event: HookEventWrapper<void>) => {
|
||||
if (event.detail.hook === DataChannelHooks.DATA_CHANNEL) {
|
||||
if (event.detail.hook === DataChannelHooks.DATA_CHANNEL_BUILDER) {
|
||||
const eventDetails = event.detail as SubscribedEventDetails;
|
||||
const hookArguments = eventDetails?.hookArguments as DataChannelArguments | undefined;
|
||||
if (hookArguments?.channelName && hookArguments?.pluginName) {
|
||||
@ -52,7 +52,7 @@ const PluginDataChannelManager: React.ElementType<PluginDataChannelManagerProps>
|
||||
}) as EventListener;
|
||||
const unsubscribeHandler: EventListener = (
|
||||
(event: HookEventWrapper<void>) => {
|
||||
if (event.detail.hook === DataChannelHooks.DATA_CHANNEL) {
|
||||
if (event.detail.hook === DataChannelHooks.DATA_CHANNEL_BUILDER) {
|
||||
const eventDetails = event.detail as UnsubscribedEventDetails;
|
||||
const hookArguments = eventDetails?.hookArguments as DataChannelArguments | undefined;
|
||||
if (hookArguments?.channelName && hookArguments?.pluginName) {
|
||||
|
@ -0,0 +1,34 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const PLUGIN_DATA_CHANNEL_DISPATCH_MUTATION = gql`
|
||||
mutation PluginDataChannelDispatchMessage($pluginName: String!,
|
||||
$dataChannel: String!, $payloadJson: String!, $toRoles: [String]!,
|
||||
$toUserIds: [String]!) {
|
||||
pluginDataChannelDispatchMessage(
|
||||
pluginName: $pluginName,
|
||||
dataChannel: $dataChannel,
|
||||
payloadJson: $payloadJson,
|
||||
toRoles: $toRoles,
|
||||
toUserIds: $toUserIds,
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
export const PLUGIN_DATA_CHANNEL_RESET_MUTATION = gql`
|
||||
mutation PluginDataChannelReset($pluginName: String!, $dataChannel: String!) {
|
||||
pluginDataChannelReset(
|
||||
pluginName: $pluginName,
|
||||
dataChannel: $dataChannel
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
export const PLUGIN_DATA_CHANNEL_DELETE_MUTATION = gql`
|
||||
mutation PluginDataChannelDeleteMessage($pluginName: String!, $dataChannel: String!, $messageId: String!) {
|
||||
pluginDataChannelDeleteMessage(
|
||||
pluginName: $pluginName,
|
||||
dataChannel: $dataChannel,
|
||||
messageId: $messageId
|
||||
)
|
||||
}
|
||||
`;
|
@ -1,18 +1,5 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
const PLUGIN_DATA_CHANNEL_DISPATCH_QUERY = gql`
|
||||
mutation DispatchPluginDataChannelMessageMsg($pluginName: String!,
|
||||
$dataChannel: String!, $payloadJson: String!, $toRoles: [String]!, $toUserIds: [String]!) {
|
||||
dispatchPluginDataChannelMessageMsg(
|
||||
pluginName: $pluginName,
|
||||
dataChannel: $dataChannel,
|
||||
payloadJson: $payloadJson,
|
||||
toRoles: $toRoles,
|
||||
toUserIds: $toUserIds,
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
const PLUGIN_DATA_CHANNEL_FETCH_QUERY = gql`
|
||||
subscription FetchPluginDataChannelMessageMsg($pluginName: String!, $channelName: String!){
|
||||
pluginDataChannelMessage(
|
||||
@ -33,4 +20,4 @@ const PLUGIN_DATA_CHANNEL_FETCH_QUERY = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export { PLUGIN_DATA_CHANNEL_DISPATCH_QUERY, PLUGIN_DATA_CHANNEL_FETCH_QUERY };
|
||||
export default PLUGIN_DATA_CHANNEL_FETCH_QUERY;
|
||||
|
@ -0,0 +1,56 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
|
||||
import {
|
||||
HookEvents,
|
||||
} from 'bigbluebutton-html-plugin-sdk/dist/cjs/core/enum';
|
||||
import { DataConsumptionHooks } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-consumption/enums';
|
||||
import { UpdatedEventDetails } from 'bigbluebutton-html-plugin-sdk/dist/cjs/core/types';
|
||||
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
||||
import formatTalkingIndicatorDataFromGraphql from './utils';
|
||||
import { UserVoice } from '/imports/ui/Types/userVoice';
|
||||
import useTalkingIndicator from '/imports/ui/core/hooks/useTalkingIndicator';
|
||||
|
||||
const TalkingIndicatorHookContainer = () => {
|
||||
const [sendSignal, setSendSignal] = useState(false);
|
||||
const userVoice: GraphqlDataHookSubscriptionResponse<Partial<UserVoice>[]> = useTalkingIndicator(
|
||||
(uv: Partial<UserVoice>) => ({
|
||||
talking: uv.talking,
|
||||
startTime: uv.startTime,
|
||||
muted: uv.muted,
|
||||
userId: uv.userId,
|
||||
}) as Partial<UserVoice>,
|
||||
);
|
||||
|
||||
const updateTalkingIndicatorForPlugin = () => {
|
||||
window.dispatchEvent(new CustomEvent<
|
||||
UpdatedEventDetails<PluginSdk.GraphqlResponseWrapper<PluginSdk.UserVoice>>
|
||||
>(HookEvents.UPDATED, {
|
||||
detail: {
|
||||
data: formatTalkingIndicatorDataFromGraphql(userVoice),
|
||||
hook: DataConsumptionHooks.TALKING_INDICATOR,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateTalkingIndicatorForPlugin();
|
||||
}, [userVoice, sendSignal]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateHookUseTalkingIndicator = () => {
|
||||
setSendSignal(!sendSignal);
|
||||
};
|
||||
window.addEventListener(
|
||||
HookEvents.SUBSCRIBED, updateHookUseTalkingIndicator,
|
||||
);
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
HookEvents.SUBSCRIBED, updateHookUseTalkingIndicator,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default TalkingIndicatorHookContainer;
|
@ -0,0 +1,18 @@
|
||||
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
||||
import { UserVoice } from '/imports/ui/Types/userVoice';
|
||||
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
|
||||
|
||||
const formatTalkingIndicatorDataFromGraphql = (
|
||||
responseDataFromGraphql: GraphqlDataHookSubscriptionResponse<Partial<UserVoice>[]>,
|
||||
) => ({
|
||||
data: !responseDataFromGraphql.loading ? responseDataFromGraphql.data?.map((userVoice) => ({
|
||||
talking: userVoice.talking,
|
||||
startTime: userVoice.startTime,
|
||||
muted: userVoice.muted,
|
||||
userId: userVoice.userId,
|
||||
}) as PluginSdk.UserVoice) : undefined,
|
||||
loading: responseDataFromGraphql.loading,
|
||||
error: responseDataFromGraphql.errors?.[0],
|
||||
} as PluginSdk.GraphqlResponseWrapper<PluginSdk.UserVoice>);
|
||||
|
||||
export default formatTalkingIndicatorDataFromGraphql;
|
@ -17,10 +17,12 @@ import CustomSubscriptionHookContainer from './domain/shared/custom-subscription
|
||||
import { ObjectToCustomHookContainerMap, HookWithArgumentsContainerProps, HookWithArgumentContainerToRender } from './domain/shared/custom-subscription/types';
|
||||
import CurrentPresentationHookContainer from './domain/presentations/current-presentation/hook-manager';
|
||||
import LoadedChatMessagesHookContainer from './domain/chat/loaded-chat-messages/hook-manager';
|
||||
import TalkingIndicatorHookContainer from './domain/user-voice/talking-indicator/hook-manager';
|
||||
|
||||
const hooksMap:{
|
||||
[key: string]: React.FunctionComponent
|
||||
} = {
|
||||
[DataConsumptionHooks.TALKING_INDICATOR]: TalkingIndicatorHookContainer,
|
||||
[DataConsumptionHooks.LOADED_CHAT_MESSAGES]: LoadedChatMessagesHookContainer,
|
||||
[DataConsumptionHooks.LOADED_USER_LIST]: LoadedUserListHookContainer,
|
||||
[DataConsumptionHooks.CURRENT_USER]: CurrentUserHookContainer,
|
||||
|
@ -14,7 +14,6 @@ import {
|
||||
import {
|
||||
colorText,
|
||||
colorBlueLight,
|
||||
colorGray,
|
||||
colorGrayLight,
|
||||
colorGrayLighter,
|
||||
colorGrayLightest,
|
||||
@ -23,8 +22,6 @@ import {
|
||||
colorHeading,
|
||||
colorPrimary,
|
||||
colorGrayDark,
|
||||
colorWhite,
|
||||
pollBlue,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { fontSizeBase, fontSizeSmall } from '/imports/ui/stylesheets/styled-components/typography';
|
||||
|
||||
@ -260,71 +257,7 @@ const NoSlidePanelContainer = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const PollButton = styled(Button)`
|
||||
margin-top: ${smPaddingY};
|
||||
margin-bottom: ${smPaddingY};
|
||||
background-color: ${colorWhite};
|
||||
box-shadow: 0 0 0 1px ${colorGray};
|
||||
color: ${colorGray};
|
||||
|
||||
& > span {
|
||||
color: ${colorGray};
|
||||
}
|
||||
|
||||
& > span:hover {
|
||||
color: ${pollBlue};
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: ${colorWhite};
|
||||
box-shadow: 0 0 0 1px ${pollBlue};
|
||||
|
||||
& > span {
|
||||
color: ${pollBlue};
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: ${colorWhite};
|
||||
box-shadow: 0 0 0 1px ${pollBlue};
|
||||
|
||||
& > span {
|
||||
color: ${pollBlue};
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
margin-right: inherit;
|
||||
margin-left: ${smPaddingY};
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin-right: ${smPaddingY};
|
||||
margin-left: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(odd) {
|
||||
margin-right: 1rem;
|
||||
margin-left: inherit;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin-right: inherit;
|
||||
margin-left: ${smPaddingY};
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px ${pollBlue};
|
||||
background-color: ${colorWhite};
|
||||
color: ${pollBlue};
|
||||
|
||||
& > span {
|
||||
color: ${pollBlue};
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
const PollButton = styled(Button)``;
|
||||
|
||||
const DragAndDropPollContainer = styled.div`
|
||||
width: 200px !important;
|
||||
|
@ -840,6 +840,7 @@ class Presentation extends PureComponent {
|
||||
height: svgDimensions.height < 0 ? 0 : svgDimensions.height,
|
||||
textAlign: 'center',
|
||||
display: !presentationIsOpen ? 'none' : 'block',
|
||||
zIndex: 1,
|
||||
}}
|
||||
id="presentationInnerWrapper"
|
||||
>
|
||||
|
@ -38,7 +38,7 @@ const ButtonWrapper = styled.div`
|
||||
background-color: ${colorTransparent};
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
z-index: 2;
|
||||
z-index: 999;
|
||||
margin: 2px;
|
||||
bottom: 0;
|
||||
|
||||
|
@ -23,7 +23,81 @@ export const PRESENTATION_SET_WRITERS = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const PRESENTATION_SET_PAGE = gql`
|
||||
mutation PresentationSetPage($presentationId: String!, $pageId: String!) {
|
||||
presentationSetPage(
|
||||
presentationId: $presentationId,
|
||||
pageId: $pageId,
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
export const PRESENTATION_SET_DOWNLOADABLE = gql`
|
||||
mutation PresentationSetDownloadable(
|
||||
$presentationId: String!,
|
||||
$downloadable: Boolean!,
|
||||
$fileStateType: String!,) {
|
||||
presentationSetDownloadable(
|
||||
presentationId: $presentationId,
|
||||
downloadable: $downloadable,
|
||||
fileStateType: $fileStateType,
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
export const PRESENTATION_EXPORT = gql`
|
||||
mutation PresentationExport(
|
||||
$presentationId: String!,
|
||||
$fileStateType: String!,) {
|
||||
presentationExport(
|
||||
presentationId: $presentationId,
|
||||
fileStateType: $fileStateType,
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
export const PRESENTATION_SET_CURRENT = gql`
|
||||
mutation PresentationSetCurrent($presentationId: String!) {
|
||||
presentationSetCurrent(
|
||||
presentationId: $presentationId,
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
export const PRESENTATION_REMOVE = gql`
|
||||
mutation PresentationRemove($presentationId: String!) {
|
||||
presentationRemove(
|
||||
presentationId: $presentationId,
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
export const PRES_ANNOTATION_DELETE = gql`
|
||||
mutation PresAnnotationDelete($pageId: String!, $annotationsIds: [String]!) {
|
||||
presAnnotationDelete(
|
||||
pageId: $pageId,
|
||||
annotationsIds: $annotationsIds,
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
export const PRES_ANNOTATION_SUBMIT = gql`
|
||||
mutation PresAnnotationSubmit($pageId: String!, $annotations: json!) {
|
||||
presAnnotationSubmit(
|
||||
pageId: $pageId,
|
||||
annotations: $annotations,
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
PRESENTATION_SET_ZOOM,
|
||||
PRESENTATION_SET_WRITERS,
|
||||
PRESENTATION_SET_PAGE,
|
||||
PRESENTATION_SET_DOWNLOADABLE,
|
||||
PRESENTATION_EXPORT,
|
||||
PRESENTATION_SET_CURRENT,
|
||||
PRESENTATION_REMOVE,
|
||||
PRES_ANNOTATION_DELETE,
|
||||
PRES_ANNOTATION_SUBMIT,
|
||||
};
|
||||
|
@ -88,9 +88,15 @@ const propTypes = {
|
||||
layoutContextDispatch: PropTypes.func.isRequired,
|
||||
isRTL: PropTypes.bool,
|
||||
tldrawAPI: PropTypes.shape({
|
||||
copySvg: PropTypes.func.isRequired,
|
||||
getShapes: PropTypes.func.isRequired,
|
||||
currentPageId: PropTypes.string.isRequired,
|
||||
getSvg: PropTypes.func.isRequired,
|
||||
currentPageShapes: PropTypes.arrayOf(PropTypes.shape({
|
||||
x: PropTypes.number.isRequired,
|
||||
y: PropTypes.number.isRequired,
|
||||
props: PropTypes.shape({
|
||||
w: PropTypes.number.isRequired,
|
||||
h: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
})).isRequired,
|
||||
}),
|
||||
presentationDropdownItems: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
@ -307,21 +313,15 @@ const PresentationMenu = (props) => {
|
||||
AppService.setDarkTheme(false);
|
||||
|
||||
try {
|
||||
const { copySvg, getShape, getShapes, currentPageId } = tldrawAPI;
|
||||
|
||||
// filter shapes that are inside the slide
|
||||
const backgroundShape = getShape('slide-background-shape');
|
||||
const shapes = getShapes(currentPageId)
|
||||
.filter((shape) =>
|
||||
shape.point[0] <= backgroundShape.size[0] &&
|
||||
shape.point[1] <= backgroundShape.size[1] &&
|
||||
shape.point[0] >= 0 &&
|
||||
shape.point[1] >= 0
|
||||
);
|
||||
const svgString = await copySvg(shapes.map((shape) => shape.id));
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = svgString;
|
||||
const svgElem = container.firstChild;
|
||||
const backgroundShape = tldrawAPI.currentPageShapes.find((s) => s.id === `shape:BG-${slideNum}`);
|
||||
const shapes = tldrawAPI.currentPageShapes.filter(
|
||||
(shape) => shape.x <= backgroundShape.props.w
|
||||
&& shape.y <= backgroundShape.props.h
|
||||
&& shape.x >= 0
|
||||
&& shape.y >= 0,
|
||||
);
|
||||
const svgElem = await tldrawAPI.getSvg(shapes.map((shape) => shape.id));
|
||||
const width = svgElem?.width?.baseVal?.value ?? window.screen.width;
|
||||
const height = svgElem?.height?.baseVal?.value ?? window.screen.height;
|
||||
|
||||
@ -358,7 +358,7 @@ const PresentationMenu = (props) => {
|
||||
);
|
||||
}
|
||||
|
||||
const tools = document.querySelector('#TD-Tools');
|
||||
const tools = document.querySelector('.tlui-toolbar, .tlui-style-panel__wrapper, .tlui-menu-zone');
|
||||
if (tools && (props.hasWBAccess || props.amIPresenter)){
|
||||
menuItems.push(
|
||||
{
|
||||
|
@ -150,12 +150,12 @@ class PresentationToolbar extends PureComponent {
|
||||
}
|
||||
|
||||
handleSkipToSlideChange(event) {
|
||||
const { skipToSlide, presentationId } = this.props;
|
||||
const { skipToSlide } = this.props;
|
||||
const requestedSlideNum = Number.parseInt(event.target.value, 10);
|
||||
|
||||
this.handleFTWSlideChange();
|
||||
if (event) event.currentTarget.blur();
|
||||
skipToSlide(requestedSlideNum, presentationId);
|
||||
skipToSlide(requestedSlideNum);
|
||||
}
|
||||
|
||||
handleSwitchWhiteboardMode() {
|
||||
@ -194,29 +194,21 @@ class PresentationToolbar extends PureComponent {
|
||||
}
|
||||
|
||||
nextSlideHandler(event) {
|
||||
const {
|
||||
nextSlide,
|
||||
currentSlideNum,
|
||||
numberOfSlides,
|
||||
endCurrentPoll,
|
||||
presentationId,
|
||||
} = this.props;
|
||||
const { nextSlide, endCurrentPoll } = this.props;
|
||||
|
||||
this.handleFTWSlideChange();
|
||||
if (event) event.currentTarget.blur();
|
||||
endCurrentPoll();
|
||||
nextSlide(currentSlideNum, numberOfSlides, presentationId);
|
||||
nextSlide();
|
||||
}
|
||||
|
||||
previousSlideHandler(event) {
|
||||
const {
|
||||
previousSlide, currentSlideNum, endCurrentPoll, presentationId
|
||||
} = this.props;
|
||||
const { previousSlide, endCurrentPoll } = this.props;
|
||||
|
||||
this.handleFTWSlideChange();
|
||||
if (event) event.currentTarget.blur();
|
||||
endCurrentPoll();
|
||||
previousSlide(currentSlideNum, presentationId);
|
||||
previousSlide();
|
||||
}
|
||||
|
||||
switchSlide(event) {
|
||||
|
@ -2,19 +2,24 @@ import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import PresentationToolbar from './component';
|
||||
import PresentationToolbarService from './service';
|
||||
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
|
||||
import { isPollingEnabled } from '/imports/ui/services/features';
|
||||
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
|
||||
import { useSubscription, useMutation } from '@apollo/client';
|
||||
import POLL_SUBSCRIPTION from '/imports/ui/core/graphql/queries/pollSubscription';
|
||||
import { POLL_CANCEL, POLL_CREATE } from '/imports/ui/components/poll/mutations';
|
||||
import { PRESENTATION_SET_PAGE } from '../mutations';
|
||||
|
||||
const PresentationToolbarContainer = (props) => {
|
||||
const pluginsContext = useContext(PluginsContext);
|
||||
const { pluginsExtensibleAreasAggregatedState } = pluginsContext;
|
||||
|
||||
const { userIsPresenter, layoutSwapped } = props;
|
||||
const {
|
||||
userIsPresenter,
|
||||
layoutSwapped,
|
||||
currentSlideNum,
|
||||
presentationId,
|
||||
} = props;
|
||||
|
||||
const { data: pollData } = useSubscription(POLL_SUBSCRIPTION);
|
||||
const hasPoll = pollData?.poll?.length > 0;
|
||||
@ -23,11 +28,36 @@ const PresentationToolbarContainer = (props) => {
|
||||
|
||||
const [stopPoll] = useMutation(POLL_CANCEL);
|
||||
const [createPoll] = useMutation(POLL_CREATE);
|
||||
const [presentationSetPage] = useMutation(PRESENTATION_SET_PAGE);
|
||||
|
||||
const endCurrentPoll = () => {
|
||||
if (hasPoll) stopPoll();
|
||||
};
|
||||
|
||||
const setPresentationPage = (pageId) => {
|
||||
presentationSetPage({
|
||||
variables: {
|
||||
presentationId,
|
||||
pageId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const skipToSlide = (slideNum) => {
|
||||
const slideId = `${presentationId}/${slideNum}`;
|
||||
setPresentationPage(slideId);
|
||||
};
|
||||
|
||||
const previousSlide = () => {
|
||||
const prevSlideNum = currentSlideNum - 1;
|
||||
skipToSlide(prevSlideNum);
|
||||
};
|
||||
|
||||
const nextSlide = () => {
|
||||
const nextSlideNum = currentSlideNum + 1;
|
||||
skipToSlide(nextSlideNum);
|
||||
};
|
||||
|
||||
const startPoll = (pollType, pollId, answers = [], question, isMultipleResponse = false) => {
|
||||
Session.set('openPanel', 'poll');
|
||||
Session.set('forcePollOpen', true);
|
||||
@ -60,6 +90,9 @@ const PresentationToolbarContainer = (props) => {
|
||||
pluginProvidedPresentationToolbarItems,
|
||||
handleToggleFullScreen,
|
||||
startPoll,
|
||||
previousSlide,
|
||||
nextSlide,
|
||||
skipToSlide,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@ -69,9 +102,6 @@ const PresentationToolbarContainer = (props) => {
|
||||
|
||||
export default withTracker(() => {
|
||||
return {
|
||||
nextSlide: PresentationToolbarService.nextSlide,
|
||||
previousSlide: PresentationToolbarService.previousSlide,
|
||||
skipToSlide: PresentationToolbarService.skipToSlide,
|
||||
isMeteorConnected: Meteor.status().connected,
|
||||
isPollingEnabled: isPollingEnabled(),
|
||||
};
|
||||
|
@ -1,25 +0,0 @@
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
|
||||
const POD_ID = 'DEFAULT_PRESENTATION_POD';
|
||||
|
||||
const previousSlide = (currentSlideNum, presentationId) => {
|
||||
if (currentSlideNum > 1) {
|
||||
makeCall('switchSlide', currentSlideNum - 1, POD_ID, presentationId);
|
||||
}
|
||||
};
|
||||
|
||||
const nextSlide = (currentSlideNum, numberOfSlides, presentationId) => {
|
||||
if (currentSlideNum < numberOfSlides) {
|
||||
makeCall('switchSlide', currentSlideNum + 1, POD_ID, presentationId);
|
||||
}
|
||||
};
|
||||
|
||||
const skipToSlide = (requestedSlideNum, presentationId) => {
|
||||
makeCall('switchSlide', requestedSlideNum, POD_ID, presentationId);
|
||||
};
|
||||
|
||||
export default {
|
||||
nextSlide,
|
||||
previousSlide,
|
||||
skipToSlide,
|
||||
};
|
@ -583,6 +583,8 @@ class PresentationUploader extends Component {
|
||||
selectedToBeNextCurrent,
|
||||
presentations: propPresentations,
|
||||
dispatchChangePresentationDownloadable,
|
||||
setPresentation,
|
||||
removePresentation,
|
||||
} = this.props;
|
||||
const { disableActions, presentations } = this.state;
|
||||
const presentationsToSave = presentations;
|
||||
@ -610,7 +612,14 @@ class PresentationUploader extends Component {
|
||||
|
||||
if (!disableActions) {
|
||||
Session.set('showUploadPresentationView', false);
|
||||
return handleSave(presentationsToSave, true, {}, propPresentations)
|
||||
return handleSave(
|
||||
presentationsToSave,
|
||||
true,
|
||||
{},
|
||||
propPresentations,
|
||||
setPresentation,
|
||||
removePresentation,
|
||||
)
|
||||
.then(() => {
|
||||
const hasError = presentations.some((p) => !!p.uploadErrorMsgKey);
|
||||
if (!hasError) {
|
||||
@ -832,9 +841,9 @@ class PresentationUploader extends Component {
|
||||
const isExporting = item?.exportToChatStatus === 'RUNNING';
|
||||
|
||||
const shouldDisableExportButton = (isExporting
|
||||
|| item.uploadInProgress
|
||||
|| !item.uploadCompleted
|
||||
|| hasError
|
||||
|| disableActions) && item.uploadInProgress;
|
||||
|| disableActions);
|
||||
|
||||
const formattedDownloadLabel = isExporting
|
||||
? intl.formatMessage(intlMessages.exporting)
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import ErrorBoundary from '/imports/ui/components/common/error-boundary/component';
|
||||
import FallbackModal from '/imports/ui/components/common/fallback-errors/fallback-modal/component';
|
||||
import { useSubscription, useMutation } from '@apollo/client';
|
||||
import Service from './service';
|
||||
import PresUploaderToast from '/imports/ui/components/presentation/presentation-toast/presentation-uploader-toast/component';
|
||||
import PresentationUploader from './component';
|
||||
@ -13,11 +13,16 @@ import {
|
||||
isDownloadPresentationConvertedToPdfEnabled,
|
||||
isPresentationEnabled,
|
||||
} from '/imports/ui/services/features';
|
||||
import { useSubscription } from '@apollo/client';
|
||||
import {
|
||||
PRESENTATIONS_SUBSCRIPTION,
|
||||
} from '/imports/ui/components/whiteboard/queries';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import {
|
||||
PRESENTATION_SET_DOWNLOADABLE,
|
||||
PRESENTATION_EXPORT,
|
||||
PRESENTATION_SET_CURRENT,
|
||||
PRESENTATION_REMOVE,
|
||||
} from '../mutations';
|
||||
|
||||
const PRESENTATION_CONFIG = Meteor.settings.public.presentation;
|
||||
|
||||
@ -31,8 +36,36 @@ const PresentationUploaderContainer = (props) => {
|
||||
const presentations = presentationData?.pres_presentation || [];
|
||||
const currentPresentation = presentations.find((p) => p.current)?.presentationId || '';
|
||||
|
||||
const [presentationSetDownloadable] = useMutation(PRESENTATION_SET_DOWNLOADABLE);
|
||||
const [presentationExport] = useMutation(PRESENTATION_EXPORT);
|
||||
const [presentationSetCurrent] = useMutation(PRESENTATION_SET_CURRENT);
|
||||
const [presentationRemove] = useMutation(PRESENTATION_REMOVE);
|
||||
|
||||
const exportPresentation = (presentationId, fileStateType) => {
|
||||
makeCall('exportPresentation', presentationId, fileStateType);
|
||||
presentationExport({
|
||||
variables: {
|
||||
presentationId,
|
||||
fileStateType,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const dispatchChangePresentationDownloadable = (presentationId, downloadable, fileStateType) => {
|
||||
presentationSetDownloadable({
|
||||
variables: {
|
||||
presentationId,
|
||||
downloadable,
|
||||
fileStateType,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const setPresentation = (presentationId) => {
|
||||
presentationSetCurrent({ variables: { presentationId } });
|
||||
};
|
||||
|
||||
const removePresentation = (presentationId) => {
|
||||
presentationRemove({ variables: { presentationId } });
|
||||
};
|
||||
|
||||
return userIsPresenter && (
|
||||
@ -42,6 +75,9 @@ const PresentationUploaderContainer = (props) => {
|
||||
presentations={presentations}
|
||||
currentPresentation={currentPresentation}
|
||||
exportPresentation={exportPresentation}
|
||||
dispatchChangePresentationDownloadable={dispatchChangePresentationDownloadable}
|
||||
setPresentation={setPresentation}
|
||||
removePresentation={removePresentation}
|
||||
{...props}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
@ -52,7 +88,6 @@ export default withTracker(() => {
|
||||
const {
|
||||
dispatchDisableDownloadable,
|
||||
dispatchEnableDownloadable,
|
||||
dispatchChangePresentationDownloadable,
|
||||
} = Service;
|
||||
const isOpen = isPresentationEnabled() && (Session.get('showUploadPresentationView') || false);
|
||||
|
||||
@ -70,7 +105,6 @@ export default withTracker(() => {
|
||||
renderPresentationItemStatus: PresUploaderToast.renderPresentationItemStatus,
|
||||
dispatchDisableDownloadable,
|
||||
dispatchEnableDownloadable,
|
||||
dispatchChangePresentationDownloadable,
|
||||
isOpen,
|
||||
selectedToBeNextCurrent: Session.get('selectedToBeNextCurrent') || null,
|
||||
externalUploadData: Service.getExternalUploadData(),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user