Merge remote-tracking branch 'upstream/v3.0.x-release' into PR_19901
This commit is contained in:
commit
0210fcdd81
@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.breakout
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.{ BreakoutModel, PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.db.UserDAO
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.models.VoiceUsers
|
||||
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
|
||||
@ -17,13 +18,13 @@ trait TransferUserToMeetingRequestHdlr extends RightsManagementTrait {
|
||||
val reason = "No permission to transfer user to voice breakout."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
|
||||
} else {
|
||||
processRequest(msg)
|
||||
processTransferUserToMeetingRequest(msg)
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
def processRequest(msg: TransferUserToMeetingRequestMsg) {
|
||||
def processTransferUserToMeetingRequest(msg: TransferUserToMeetingRequestMsg) {
|
||||
if (msg.body.fromMeetingId == liveMeeting.props.meetingProp.intId) {
|
||||
// want to transfer from parent meeting to breakout
|
||||
for {
|
||||
@ -32,6 +33,7 @@ trait TransferUserToMeetingRequestHdlr extends RightsManagementTrait {
|
||||
from <- getVoiceConf(msg.body.fromMeetingId, model)
|
||||
voiceUser <- VoiceUsers.findWithIntId(liveMeeting.voiceUsers, msg.body.userId)
|
||||
} yield {
|
||||
UserDAO.transferUserToBreakoutRoomAsAudioOnly(msg.body.userId, msg.body.fromMeetingId, msg.body.toMeetingId)
|
||||
val event = buildTransferUserToVoiceConfSysMsg(from, to, voiceUser.voiceUserId)
|
||||
outGW.send(event)
|
||||
}
|
||||
@ -53,6 +55,7 @@ trait TransferUserToMeetingRequestHdlr extends RightsManagementTrait {
|
||||
room <- model.find(msg.body.fromMeetingId)
|
||||
voiceUser <- room.voiceUsers.find(p => p.id == msg.body.userId)
|
||||
} yield {
|
||||
UserDAO.transferUserToBreakoutRoomAsAudioOnly(msg.body.userId, msg.body.fromMeetingId, msg.body.toMeetingId)
|
||||
val event = buildTransferUserToVoiceConfSysMsg(from, to, voiceUser.voiceUserId)
|
||||
outGW.send(event)
|
||||
}
|
||||
|
@ -4,8 +4,9 @@ import org.apache.pekko.actor.ActorContext
|
||||
import org.apache.pekko.event.Logging
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting }
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.db.{ CaptionLocaleDAO, CaptionTypes }
|
||||
|
||||
class CaptionApp2x(implicit val context: ActorContext) extends RightsManagementTrait {
|
||||
val log = Logging(context.system, getClass)
|
||||
@ -84,6 +85,7 @@ class CaptionApp2x(implicit val context: ActorContext) extends RightsManagementT
|
||||
val event = UpdateCaptionOwnerEvtMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
bus.outGW.send(msgEvent)
|
||||
CaptionLocaleDAO.insertOrUpdateCaptionLocale(liveMeeting.props.meetingProp.intId, locale, CaptionTypes.TYPED, newOwnerId)
|
||||
}
|
||||
|
||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.db.UserDAO
|
||||
import org.bigbluebutton.core.models.{ Users2x, VoiceUserState, VoiceUsers }
|
||||
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
|
||||
|
||||
@ -29,11 +30,10 @@ trait UserConnectedToGlobalAudioMsgHdlr {
|
||||
for {
|
||||
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
|
||||
} yield {
|
||||
|
||||
val vu = VoiceUserState(
|
||||
intId = user.intId,
|
||||
voiceUserId = user.intId,
|
||||
meetingId = user.meetingId,
|
||||
meetingId = props.meetingProp.intId,
|
||||
callingWith = "flash",
|
||||
callerName = user.name,
|
||||
callerNum = user.name,
|
||||
|
@ -302,7 +302,6 @@ object VoiceApp extends SystemConfiguration {
|
||||
)
|
||||
outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
checkAndEjectOldDuplicateVoiceConfUser(intId, liveMeeting, outGW)
|
||||
|
||||
val isListenOnly = if (callerIdName.startsWith("LISTENONLY")) true else false
|
||||
|
@ -9,7 +9,7 @@ case class CaptionDbModel(
|
||||
meetingId: String,
|
||||
captionType: String,
|
||||
userId: String,
|
||||
lang: String,
|
||||
locale: String,
|
||||
captionText: String,
|
||||
createdAt: java.sql.Timestamp
|
||||
)
|
||||
@ -19,10 +19,10 @@ class CaptionTableDef(tag: Tag) extends Table[CaptionDbModel](tag, None, "captio
|
||||
val meetingId = column[String]("meetingId")
|
||||
val captionType = column[String]("captionType")
|
||||
val userId = column[String]("userId")
|
||||
val lang = column[String]("lang")
|
||||
val locale = column[String]("locale")
|
||||
val captionText = column[String]("captionText")
|
||||
val createdAt = column[java.sql.Timestamp]("createdAt")
|
||||
def * = (captionId, meetingId, captionType, userId, lang, captionText, createdAt) <> (CaptionDbModel.tupled, CaptionDbModel.unapply)
|
||||
def * = (captionId, meetingId, captionType, userId, locale, captionText, createdAt) <> (CaptionDbModel.tupled, CaptionDbModel.unapply)
|
||||
}
|
||||
|
||||
object CaptionTypes {
|
||||
@ -32,7 +32,7 @@ object CaptionTypes {
|
||||
|
||||
object CaptionDAO {
|
||||
|
||||
def insertOrUpdateAudioCaption(captionId: String, meetingId: String, userId: String, transcript: String, lang: String) = {
|
||||
def insertOrUpdateAudioCaption(captionId: String, meetingId: String, userId: String, transcript: String, locale: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[CaptionTableDef].insertOrUpdate(
|
||||
CaptionDbModel(
|
||||
@ -40,7 +40,7 @@ object CaptionDAO {
|
||||
meetingId = meetingId,
|
||||
captionType = CaptionTypes.AUDIO_TRANSCRIPTION,
|
||||
userId = userId,
|
||||
lang = lang,
|
||||
locale = locale,
|
||||
captionText = transcript,
|
||||
createdAt = new java.sql.Timestamp(System.currentTimeMillis())
|
||||
)
|
||||
@ -68,7 +68,7 @@ object CaptionDAO {
|
||||
SELECT "captionId", "captionText", "createdAt"
|
||||
FROM caption
|
||||
WHERE "meetingId" = ${meetingId}
|
||||
AND lang = ${locale}
|
||||
AND locale = ${locale}
|
||||
AND "captionType" = ${CaptionTypes.TYPED}
|
||||
order by "createdAt" desc
|
||||
limit 2
|
||||
@ -80,13 +80,13 @@ object CaptionDAO {
|
||||
LIMIT 1
|
||||
)
|
||||
RETURNING *)
|
||||
INSERT INTO caption ("captionId", "meetingId", "captionType", "userId", "lang", "captionText", "createdAt")
|
||||
INSERT INTO caption ("captionId", "meetingId", "captionType", "userId", "locale", "captionText", "createdAt")
|
||||
SELECT md5(random()::text || clock_timestamp()::text), ${meetingId}, 'TYPED', ${userId}, ${locale}, ${line}, current_timestamp
|
||||
WHERE NOT EXISTS (SELECT * FROM upsert)
|
||||
AND ${line} NOT IN (SELECT "captionText"
|
||||
FROM caption
|
||||
WHERE "meetingId" = ${meetingId}
|
||||
AND lang = ${locale}
|
||||
AND locale = ${locale}
|
||||
AND "captionType" = ${CaptionTypes.TYPED}
|
||||
order by "createdAt" desc
|
||||
limit 2
|
||||
|
41
akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/CaptionLangDAO.scala
Executable file
41
akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/CaptionLangDAO.scala
Executable file
@ -0,0 +1,41 @@
|
||||
package org.bigbluebutton.core.db
|
||||
|
||||
import slick.jdbc.PostgresProfile.api._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{ Failure, Success }
|
||||
|
||||
case class CaptionLocaleDbModel(
|
||||
meetingId: String,
|
||||
locale: String,
|
||||
captionType: String,
|
||||
ownerUserId: String,
|
||||
updatedAt: java.sql.Timestamp
|
||||
)
|
||||
|
||||
class CaptionLocaleTableDef(tag: Tag) extends Table[CaptionLocaleDbModel](tag, None, "caption_locale") {
|
||||
val meetingId = column[String]("meetingId", O.PrimaryKey)
|
||||
val locale = column[String]("locale", O.PrimaryKey)
|
||||
val captionType = column[String]("captionType", O.PrimaryKey)
|
||||
val ownerUserId = column[String]("ownerUserId")
|
||||
val updatedAt = column[java.sql.Timestamp]("updatedAt")
|
||||
def * = (meetingId, locale, captionType, ownerUserId, updatedAt) <> (CaptionLocaleDbModel.tupled, CaptionLocaleDbModel.unapply)
|
||||
}
|
||||
|
||||
object CaptionLocaleDAO {
|
||||
def insertOrUpdateCaptionLocale(meetingId: String, locale: String, captionType: String, ownerUserId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[CaptionLocaleTableDef].insertOrUpdate(
|
||||
CaptionLocaleDbModel(
|
||||
meetingId = meetingId,
|
||||
locale = locale,
|
||||
captionType = captionType,
|
||||
ownerUserId = ownerUserId,
|
||||
updatedAt = new java.sql.Timestamp(System.currentTimeMillis())
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(_) => DatabaseConnection.logger.debug(s"Upserted caption with ID $meetingId-$locale on CaptionLocale table")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error upserting caption on CaptionLocale: $e")
|
||||
}
|
||||
}
|
||||
}
|
@ -163,6 +163,62 @@ object UserDAO {
|
||||
}
|
||||
}
|
||||
|
||||
def transferUserToBreakoutRoomAsAudioOnly(userId: String, meetingIdFrom: String, meetingIdTo: String) = {
|
||||
|
||||
//Create a copy of the user using the same userId, but with the meetingId of the breakoutRoom
|
||||
//The user will be flagged by `transferredFromParentMeeting=true`
|
||||
DatabaseConnection.db.run(
|
||||
sqlu"""
|
||||
WITH upsert AS (
|
||||
UPDATE "user"
|
||||
SET "loggedOut"=false
|
||||
where "userId" = ${userId}
|
||||
and "meetingId" = ${meetingIdTo}
|
||||
RETURNING *)
|
||||
insert into "user"("meetingId","userId","extId","name","role","guest","authed","guestStatus","locked",
|
||||
"color","loggedOut","expired","ejected","joined","registeredOn","transferredFromParentMeeting","clientType")
|
||||
select
|
||||
${meetingIdTo} as "meetingId",
|
||||
"userId",
|
||||
"extId",
|
||||
"name",
|
||||
"role",
|
||||
true as "guest",
|
||||
true as "authed",
|
||||
'ALLOW' as "guestStatus",
|
||||
false as "locked",
|
||||
"color",
|
||||
"loggedOut",
|
||||
"expired",
|
||||
"ejected",
|
||||
"joined",
|
||||
"registeredOn",
|
||||
true as "transferredFromParentMeeting",
|
||||
'dial-in-user' as "clientType"
|
||||
from "user"
|
||||
where "userId" = ${userId}
|
||||
and "meetingId" = ${meetingIdFrom}
|
||||
and NOT EXISTS (SELECT * FROM upsert)
|
||||
"""
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted in user (transferredFromParentMeeting) table")
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error inserting user (transferredFromParentMeeting): $e")
|
||||
}
|
||||
|
||||
//Set user as loggedOut in the old meeting (if it is from transferred origin)
|
||||
DatabaseConnection.db.run(
|
||||
sqlu"""update "user"
|
||||
set "loggedOut" = true
|
||||
where "userId" = ${userId}
|
||||
and "meetingId" = ${meetingIdFrom}
|
||||
and "transferredFromParentMeeting" is true
|
||||
"""
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated in user (transferredFromParentMeeting) table")
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error updating user (transferredFromParentMeeting): $e")
|
||||
}
|
||||
}
|
||||
|
||||
def permanentlyDeleteAllFromMeeting(meetingId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserDbTableDef]
|
||||
|
@ -470,7 +470,7 @@ class ReceivedJsonMsgHandlerActor(
|
||||
route[CheckGraphqlMiddlewareAlivePongSysMsg](meetingManagerChannel, envelope, jsonNode)
|
||||
|
||||
case _ =>
|
||||
log.error("Cannot route envelope name " + envelope.name)
|
||||
log.debug("Cannot route envelope name " + envelope.name)
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
24
bbb-graphql-actions/src/actions/captionSetOwner.ts
Normal file
24
bbb-graphql-actions/src/actions/captionSetOwner.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { RedisMessage } from '../types';
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
const eventName = `UpdateCaptionOwnerPubMsg`;
|
||||
|
||||
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 = {
|
||||
name: '',
|
||||
locale: input.locale,
|
||||
ownerId: input.ownerUserId,
|
||||
};
|
||||
|
||||
return { eventName, routing, header, body };
|
||||
}
|
@ -279,6 +279,7 @@ CREATE TABLE "user" (
|
||||
"guestLobbyMessage" text,
|
||||
"mobile" bool,
|
||||
"clientType" varchar(50),
|
||||
"transferredFromParentMeeting" bool default false, --when a user join in breakoutRoom only in audio
|
||||
"disconnected" bool default false, -- this is the old leftFlag (that was renamed), set when the user just closed the client
|
||||
"expired" bool default false, -- when it is been some time the user is disconnected
|
||||
"ejected" bool,
|
||||
@ -1619,7 +1620,7 @@ SELECT *,
|
||||
AND ("isModerator" is false OR "sendInvitationToModerators")
|
||||
THEN TRUE ELSE FALSE END "showInvitation"
|
||||
from (
|
||||
SELECT u."meetingId", u."userId", b."parentMeetingId", b."breakoutRoomId", b."freeJoin", b."sequence", b."name", b."isDefaultName",
|
||||
SELECT u."meetingId" as "userMeetingId", u."userId", b."parentMeetingId", b."breakoutRoomId", b."freeJoin", b."sequence", b."name", b."isDefaultName",
|
||||
b."shortName", b."startedAt", b."endedAt", b."durationInSeconds", b."sendInvitationToModerators",
|
||||
bu."assignedAt", bu."joinURL", bu."inviteDismissedAt", u."role" = 'MODERATOR' as "isModerator",
|
||||
--CASE WHEN b."durationInSeconds" = 0 THEN NULL ELSE b."startedAt" + b."durationInSeconds" * '1 second'::INTERVAL END AS "willEndAt",
|
||||
@ -1642,15 +1643,31 @@ from (
|
||||
) a;
|
||||
|
||||
CREATE OR REPLACE VIEW "v_breakoutRoom_assignedUser" AS
|
||||
SELECT "parentMeetingId", "breakoutRoomId", "meetingId", "userId"
|
||||
SELECT "parentMeetingId", "breakoutRoomId", "userMeetingId", "userId"
|
||||
FROM "v_breakoutRoom"
|
||||
WHERE "assignedAt" IS NOT NULL;
|
||||
|
||||
--TODO improve performance (and handle two users with same extId)
|
||||
CREATE OR REPLACE VIEW "v_breakoutRoom_participant" AS
|
||||
SELECT DISTINCT "parentMeetingId", "breakoutRoomId", "meetingId", "userId"
|
||||
CREATE OR REPLACE VIEW "v_breakoutRoom_participant" as
|
||||
SELECT DISTINCT
|
||||
"parentMeetingId",
|
||||
"breakoutRoomId",
|
||||
"userMeetingId",
|
||||
"userId",
|
||||
false as "isAudioOnly"
|
||||
FROM "v_breakoutRoom"
|
||||
WHERE "currentRoomIsOnline" IS TRUE;
|
||||
WHERE "currentRoomIsOnline" IS TRUE
|
||||
union --include users that joined only with audio
|
||||
select parent_user."meetingId" as "parentMeetingId",
|
||||
bk_user."meetingId" as "breakoutRoomId",
|
||||
parent_user."meetingId" as "userMeetingId",
|
||||
parent_user."userId",
|
||||
true as "isAudioOnly"
|
||||
from "user" bk_user
|
||||
join "user" parent_user on parent_user."userId" = bk_user."userId" and parent_user."transferredFromParentMeeting" is false
|
||||
where bk_user."transferredFromParentMeeting" is true
|
||||
and bk_user."loggedOut" is false;
|
||||
|
||||
--SELECT DISTINCT br."parentMeetingId", br."breakoutRoomId", "user"."meetingId", "user"."userId"
|
||||
--FROM v_user "user"
|
||||
--JOIN "meeting" m using("meetingId")
|
||||
@ -1731,21 +1748,51 @@ SELECT
|
||||
FLOOR(EXTRACT(EPOCH FROM current_timestamp) * 1000)::bigint AS "currentTimeMillis";
|
||||
|
||||
------------------------------------
|
||||
----audioCaption
|
||||
----audioCaption or typedCaption
|
||||
|
||||
CREATE TABLE "caption_locale" (
|
||||
"meetingId" varchar(100) NOT NULL REFERENCES "meeting"("meetingId") ON DELETE CASCADE,
|
||||
"locale" varchar(15) NOT NULL,
|
||||
"captionType" varchar(100) NOT NULL, --Audio Transcription or Typed Caption
|
||||
"ownerUserId" varchar(50),
|
||||
"createdAt" timestamp with time zone default current_timestamp,
|
||||
"updatedAt" timestamp with time zone,
|
||||
CONSTRAINT "caption_locale_pk" primary key ("meetingId","locale","captionType"),
|
||||
FOREIGN KEY ("meetingId", "ownerUserId") REFERENCES "user"("meetingId","userId") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "caption" (
|
||||
"captionId" varchar(100) NOT NULL PRIMARY KEY,
|
||||
"meetingId" varchar(100) NOT NULL REFERENCES "meeting"("meetingId") ON DELETE CASCADE,
|
||||
"captionType" varchar(100) NOT NULL, --Audio Transcription or Typed Caption
|
||||
"userId" varchar(50),
|
||||
"lang" varchar(15),
|
||||
"locale" varchar(15),
|
||||
"captionText" text,
|
||||
"createdAt" timestamp with time zone,
|
||||
FOREIGN KEY ("meetingId", "userId") REFERENCES "user"("meetingId","userId") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
create index idx_caption on caption("meetingId","lang","createdAt");
|
||||
create index idx_caption_captionType on caption("meetingId","lang","captionType","createdAt");
|
||||
CREATE OR REPLACE FUNCTION "update_caption_locale_owner_func"() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
WITH upsert AS (
|
||||
UPDATE "caption_locale" SET
|
||||
"ownerUserId" = NEW."userId",
|
||||
"updatedAt" = current_timestamp
|
||||
WHERE "meetingId"=NEW."meetingId" AND "locale"=NEW."locale" AND "captionType"= NEW."captionType"
|
||||
RETURNING *)
|
||||
INSERT INTO "caption_locale"("meetingId","locale","captionType","ownerUserId")
|
||||
SELECT NEW."meetingId", NEW."locale", NEW."captionType", NEW."userId"
|
||||
WHERE NOT EXISTS (SELECT * FROM upsert);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER "insert_caption_trigger" BEFORE INSERT ON "caption" FOR EACH ROW
|
||||
EXECUTE FUNCTION "update_caption_locale_owner_func"();
|
||||
|
||||
create index idx_caption on caption("meetingId","locale","createdAt");
|
||||
create index idx_caption_captionType on caption("meetingId","locale","captionType","createdAt");
|
||||
|
||||
CREATE OR REPLACE VIEW "v_caption" AS
|
||||
SELECT *
|
||||
@ -1753,11 +1800,11 @@ FROM "caption"
|
||||
WHERE "createdAt" > current_timestamp - INTERVAL '5 seconds';
|
||||
|
||||
CREATE OR REPLACE VIEW "v_caption_typed_activeLocales" AS
|
||||
select distinct "meetingId", "lang", "userId"
|
||||
from "caption"
|
||||
select distinct "meetingId", "locale", "ownerUserId"
|
||||
from "caption_locale"
|
||||
where "captionType" = 'TYPED';
|
||||
|
||||
create index "idx_caption_typed_activeLocales" on caption("meetingId","lang","userId") where "captionType" = 'TYPED';
|
||||
create index "idx_caption_typed_activeLocales" on caption("meetingId","locale","userId") where "captionType" = 'TYPED';
|
||||
|
||||
------------------------------------
|
||||
----
|
||||
@ -1789,7 +1836,7 @@ CREATE TABLE "notification" (
|
||||
"messageDescription" varchar(100),
|
||||
"messageValues" jsonb,
|
||||
"role" varchar(100), --MODERATOR, PRESENTER, VIEWER
|
||||
"userMeetingId" varchar(50),
|
||||
"userMeetingId" varchar(100),
|
||||
"userId" varchar(50),
|
||||
"createdAt" timestamp with time zone DEFAULT current_timestamp,
|
||||
FOREIGN KEY ("userMeetingId", "userId") REFERENCES "user"("meetingId","userId") ON DELETE CASCADE
|
||||
|
@ -559,3 +559,11 @@ input GuestUserApprovalStatus {
|
||||
status: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
captionSetOwner(
|
||||
locale: String!
|
||||
ownerUserId: String!
|
||||
): Boolean
|
||||
}
|
||||
|
||||
|
||||
|
@ -493,6 +493,12 @@ actions:
|
||||
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
|
||||
permissions:
|
||||
- role: bbb_client
|
||||
- name: captionSetOwner
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
|
||||
permissions:
|
||||
- role: bbb_client
|
||||
custom_types:
|
||||
enums: []
|
||||
input_objects:
|
||||
|
@ -52,7 +52,7 @@ select_permissions:
|
||||
- startedAt
|
||||
filter:
|
||||
_and:
|
||||
- meetingId:
|
||||
- userMeetingId:
|
||||
_eq: X-Hasura-MeetingId
|
||||
- userId:
|
||||
_eq: X-Hasura-UserId
|
||||
|
@ -11,8 +11,8 @@ object_relationships:
|
||||
using:
|
||||
manual_configuration:
|
||||
column_mapping:
|
||||
meetingId: meetingId
|
||||
userId: userId
|
||||
userMeetingId: meetingId
|
||||
insertion_order: null
|
||||
remote_table:
|
||||
name: v_user_ref
|
||||
@ -27,7 +27,7 @@ select_permissions:
|
||||
- parentMeetingId:
|
||||
_eq: X-Hasura-ModeratorInMeeting
|
||||
- _and:
|
||||
- meetingId:
|
||||
- userMeetingId:
|
||||
_eq: X-Hasura-MeetingId
|
||||
- userId:
|
||||
_eq: X-Hasura-UserId
|
||||
|
@ -11,8 +11,8 @@ object_relationships:
|
||||
using:
|
||||
manual_configuration:
|
||||
column_mapping:
|
||||
meetingId: meetingId
|
||||
userId: userId
|
||||
userMeetingId: meetingId
|
||||
insertion_order: null
|
||||
remote_table:
|
||||
name: v_user_ref
|
||||
@ -21,6 +21,7 @@ select_permissions:
|
||||
- role: bbb_client
|
||||
permission:
|
||||
columns:
|
||||
- isAudioOnly
|
||||
- userId
|
||||
filter:
|
||||
parentMeetingId:
|
||||
|
@ -26,7 +26,7 @@ select_permissions:
|
||||
- captionId
|
||||
- userId
|
||||
- captionType
|
||||
- lang
|
||||
- locale
|
||||
filter:
|
||||
meetingId:
|
||||
_eq: X-Hasura-MeetingId
|
||||
|
@ -12,7 +12,7 @@ object_relationships:
|
||||
manual_configuration:
|
||||
column_mapping:
|
||||
meetingId: meetingId
|
||||
userId: userId
|
||||
ownerUserId: userId
|
||||
insertion_order: null
|
||||
remote_table:
|
||||
name: v_user_ref
|
||||
@ -21,7 +21,7 @@ select_permissions:
|
||||
- role: bbb_client
|
||||
permission:
|
||||
columns:
|
||||
- lang
|
||||
- locale
|
||||
filter:
|
||||
meetingId:
|
||||
_eq: X-Hasura-MeetingId
|
||||
|
@ -16,7 +16,6 @@ select_permissions:
|
||||
- messageDescription
|
||||
- messageId
|
||||
- messageValues
|
||||
- notificationId
|
||||
- notificationType
|
||||
- role
|
||||
filter:
|
||||
|
@ -11,8 +11,8 @@ object_relationships:
|
||||
using:
|
||||
manual_configuration:
|
||||
column_mapping:
|
||||
meetingId: meetingId
|
||||
fromUserId: userId
|
||||
meetingId: meetingId
|
||||
insertion_order: null
|
||||
remote_table:
|
||||
name: v_user_ref
|
||||
|
@ -1,4 +1,10 @@
|
||||
#!/bin/bash
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run this script as root ( or with sudo )" ;
|
||||
exit 1;
|
||||
fi;
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
export LANGUAGE="en_US.UTF-8"
|
||||
export LC_ALL="en_US.UTF-8"
|
||||
|
@ -8,11 +8,8 @@ import UserSettings from '/imports/api/users-settings';
|
||||
import VideoStreams from '/imports/api/video-streams';
|
||||
import VoiceUsers from '/imports/api/voice-users';
|
||||
import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user';
|
||||
import Captions from '/imports/api/captions';
|
||||
import Pads, { PadsSessions, PadsUpdates } from '/imports/api/pads';
|
||||
import AuthTokenValidation from '/imports/api/auth-token-validation';
|
||||
import Breakouts from '/imports/api/breakouts';
|
||||
import BreakoutsHistory from '/imports/api/breakouts-history';
|
||||
import Meetings, {
|
||||
RecordMeetings, MeetingTimeRemaining, Notifications,
|
||||
} from '/imports/api/meetings';
|
||||
@ -30,15 +27,12 @@ export const localCollectionRegistry = {
|
||||
localVideoStreamsSync: new AbstractCollection(VideoStreams, VideoStreams),
|
||||
localVoiceUsersSync: new AbstractCollection(VoiceUsers, VoiceUsers),
|
||||
localWhiteboardMultiUserSync: new AbstractCollection(WhiteboardMultiUser, WhiteboardMultiUser),
|
||||
localCaptionsSync: new AbstractCollection(Captions, Captions),
|
||||
localPadsSync: new AbstractCollection(Pads, Pads),
|
||||
localPadsSessionsSync: new AbstractCollection(PadsSessions, PadsSessions),
|
||||
localPadsUpdatesSync: new AbstractCollection(PadsUpdates, PadsUpdates),
|
||||
localAuthTokenValidationSync: new AbstractCollection(AuthTokenValidation, AuthTokenValidation),
|
||||
localRecordMeetingsSync: new AbstractCollection(RecordMeetings, RecordMeetings),
|
||||
localMeetingTimeRemainingSync: new AbstractCollection(MeetingTimeRemaining, MeetingTimeRemaining),
|
||||
localBreakoutsSync: new AbstractCollection(Breakouts, Breakouts),
|
||||
localBreakoutsHistorySync: new AbstractCollection(BreakoutsHistory, BreakoutsHistory),
|
||||
localMeetingsSync: new AbstractCollection(Meetings, Meetings),
|
||||
localUsersSync: new AbstractCollection(Users, Users),
|
||||
localNotificationsSync: new AbstractCollection(Notifications, Notifications),
|
||||
|
@ -23,7 +23,6 @@ import logger from '/imports/startup/client/logger';
|
||||
import '/imports/ui/services/mobile-app';
|
||||
import Base from '/imports/startup/client/base';
|
||||
import JoinHandler from '../imports/ui/components/join-handler/component';
|
||||
import AuthenticatedHandler from '/imports/ui/components/authenticated-handler/component';
|
||||
import Subscriptions from '/imports/ui/components/subscriptions/component';
|
||||
import IntlStartup from '/imports/startup/client/intl';
|
||||
import ContextProviders from '/imports/ui/components/context-providers/component';
|
||||
|
@ -1,14 +0,0 @@
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import AuthTokenValidation from '/imports/api/auth-token-validation';
|
||||
|
||||
export default async function removeValidationState(meetingId, userId, connectionId) {
|
||||
const selector = {
|
||||
meetingId, userId, connectionId,
|
||||
};
|
||||
|
||||
try {
|
||||
await AuthTokenValidation.removeAsync(selector);
|
||||
} catch (error) {
|
||||
Logger.error(`Could not remove from collection AuthTokenValidation: ${error}`);
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
const collectionOptions = Meteor.isClient ? {
|
||||
connection: null,
|
||||
} : {};
|
||||
|
||||
const BreakoutsHistory = new Mongo.Collection('breakouts-history', collectionOptions);
|
||||
|
||||
if (Meteor.isServer) {
|
||||
BreakoutsHistory.createIndexAsync({ meetingId: 1 });
|
||||
}
|
||||
|
||||
export default BreakoutsHistory;
|
@ -1,6 +0,0 @@
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import handleBreakoutRoomsList from './handlers/breakoutRoomsList';
|
||||
import messageToAllSent from '/imports/api/breakouts-history/server/handlers/messageToAllSent';
|
||||
|
||||
RedisPubSub.on('BreakoutRoomsListEvtMsg', handleBreakoutRoomsList);
|
||||
RedisPubSub.on('SendMessageToAllBreakoutRoomsEvtMsg', messageToAllSent);
|
@ -1,35 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import BreakoutsHistory from '/imports/api/breakouts-history';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default async function handleBreakoutRoomsList({ body }) {
|
||||
const {
|
||||
meetingId,
|
||||
rooms,
|
||||
} = body;
|
||||
|
||||
check(meetingId, String);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
meetingId,
|
||||
rooms,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const { insertedId } = await BreakoutsHistory.upsertAsync(selector, modifier);
|
||||
|
||||
if (insertedId) {
|
||||
Logger.info(`Added rooms to breakout-history Data: meeting=${meetingId}`);
|
||||
} else {
|
||||
Logger.info(`Upserted rooms to breakout-history Data: meeting=${meetingId}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Adding rooms to the collection breakout-history: ${err}`);
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { check } from 'meteor/check';
|
||||
import BreakoutsHistory from '/imports/api/breakouts-history';
|
||||
|
||||
export default async function handleSendMessageToAllBreakoutRoomsEvtMsg({ body }, meetingId) {
|
||||
const {
|
||||
senderId,
|
||||
msg,
|
||||
totalOfRooms,
|
||||
} = body;
|
||||
|
||||
check(meetingId, String);
|
||||
check(senderId, String);
|
||||
check(msg, String);
|
||||
check(totalOfRooms, Number);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
$push: {
|
||||
broadcastMsgs: {
|
||||
senderId,
|
||||
msg,
|
||||
totalOfRooms,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const { insertedId } = await BreakoutsHistory.upsertAsync(selector, modifier);
|
||||
|
||||
if (insertedId) {
|
||||
Logger.info(`Added broadCastMsg to breakout-history Data: meeting=${meetingId}`);
|
||||
} else {
|
||||
Logger.info(`Upserted broadCastMsg to breakout-history Data: meeting=${meetingId}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Adding broadCastMsg to the collection breakout-history: ${err}`);
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
import './eventHandlers';
|
||||
import './publishers';
|
@ -1,58 +0,0 @@
|
||||
import BreakoutsHistory from '/imports/api/breakouts-history';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Users from '/imports/api/users';
|
||||
import { publicationSafeGuard } from '/imports/api/common/server/helpers';
|
||||
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
|
||||
async function breakoutsHistory() {
|
||||
const tokenValidation = await AuthTokenValidation
|
||||
.findOneAsync({ connectionId: this.connection.id });
|
||||
|
||||
if (!tokenValidation || tokenValidation.validationStatus !== ValidationStates.VALIDATED) {
|
||||
Logger.warn(`Publishing Meetings-history was requested by unauth connection ${this.connection.id}`);
|
||||
return Meetings.find({ meetingId: '' });
|
||||
}
|
||||
|
||||
const { meetingId, userId } = tokenValidation;
|
||||
Logger.debug('Publishing Breakouts-History', { meetingId, userId });
|
||||
|
||||
const User = await Users.findOneAsync({ userId, meetingId }, { fields: { userId: 1, role: 1 } });
|
||||
if (!User || User.role !== ROLE_MODERATOR) {
|
||||
return BreakoutsHistory.find({ meetingId: '' });
|
||||
}
|
||||
|
||||
check(meetingId, String);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
};
|
||||
|
||||
// Monitor this publication and stop it when user is not a moderator anymore
|
||||
const comparisonFunc = async () => {
|
||||
const user = await Users
|
||||
.findOneAsync({ userId, meetingId }, { fields: { role: 1, userId: 1 } });
|
||||
const condition = user.role === ROLE_MODERATOR;
|
||||
|
||||
if (!condition) {
|
||||
Logger.info(`conditions aren't filled anymore in publication ${this._name}:
|
||||
user.role === ROLE_MODERATOR :${condition}, user.role: ${user.role} ROLE_MODERATOR: ${ROLE_MODERATOR}`);
|
||||
}
|
||||
|
||||
return condition;
|
||||
};
|
||||
publicationSafeGuard(comparisonFunc, this);
|
||||
|
||||
return BreakoutsHistory.find(selector);
|
||||
}
|
||||
|
||||
function publish(...args) {
|
||||
const boundUsers = breakoutsHistory.bind(this);
|
||||
return boundUsers(...args);
|
||||
}
|
||||
|
||||
Meteor.publish('breakouts-history', publish);
|
@ -2,7 +2,6 @@ import Breakouts from '/imports/api/breakouts';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { check } from 'meteor/check';
|
||||
import flat from 'flat';
|
||||
import handleBreakoutRoomsListHist from '/imports/api/breakouts-history/server/handlers/breakoutRoomsList';
|
||||
|
||||
export default async function handleBreakoutRoomsList({ body }, meetingId) {
|
||||
// 0 seconds default breakout time, forces use of real expiration time
|
||||
@ -52,6 +51,5 @@ export default async function handleBreakoutRoomsList({ body }, meetingId) {
|
||||
} else {
|
||||
Logger.error(`updating breakout: ${numberAffected}`);
|
||||
}
|
||||
handleBreakoutRoomsListHist({ body });
|
||||
});
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
const collectionOptions = Meteor.isClient ? {
|
||||
connection: null,
|
||||
} : {};
|
||||
|
||||
const Captions = new Mongo.Collection('captions', collectionOptions);
|
||||
|
||||
if (Meteor.isServer) {
|
||||
Captions.createIndexAsync({ meetingId: 1, locale: 1 });
|
||||
}
|
||||
|
||||
export default Captions;
|
@ -1,4 +0,0 @@
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import captionsOwnerUpdated from './handlers/captionsOwnerUpdated';
|
||||
|
||||
RedisPubSub.on('UpdateCaptionOwnerEvtMsg', captionsOwnerUpdated);
|
@ -1,11 +0,0 @@
|
||||
import updateCaptionsOwner from '/imports/api/captions/server/modifiers/updateCaptionsOwner';
|
||||
|
||||
export default async function captionsOwnerUpdated({ header, body }) {
|
||||
const { meetingId } = header;
|
||||
const {
|
||||
locale,
|
||||
ownerId,
|
||||
} = body;
|
||||
|
||||
await updateCaptionsOwner(meetingId, locale, ownerId);
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import createCaptions from '/imports/api/captions/server/modifiers/createCaptions';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
const CAPTIONS_CONFIG = Meteor.settings.public.captions;
|
||||
const BASENAME = Meteor.settings.public.app.basename;
|
||||
const HOST = Meteor.settings.private.app.host;
|
||||
const LOCALES = Meteor.settings.private.app.localesUrl;
|
||||
const LOCALES_URL = `http://${HOST}:${process.env.PORT}${BASENAME}${LOCALES}`;
|
||||
|
||||
const init = (meetingId) => {
|
||||
axios({
|
||||
method: 'get',
|
||||
url: LOCALES_URL,
|
||||
responseType: 'json',
|
||||
}).then(async (response) => {
|
||||
const { status } = response;
|
||||
if (status !== 200) return;
|
||||
|
||||
const locales = response.data;
|
||||
await Promise.all(locales.map(async (locale) => {
|
||||
const caption = await createCaptions(meetingId, locale.locale, locale.name);
|
||||
return caption;
|
||||
}));
|
||||
}).catch((error) => Logger.error(`Could not create captions for ${meetingId}: ${error}`));
|
||||
};
|
||||
|
||||
const initCaptions = (meetingId) => {
|
||||
if (CAPTIONS_CONFIG.enabled) init(meetingId);
|
||||
};
|
||||
|
||||
export {
|
||||
initCaptions,
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
import './eventHandlers';
|
||||
import './methods';
|
||||
import './publishers';
|
@ -1,12 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import updateCaptionsOwner from '/imports/api/captions/server/methods/updateCaptionsOwner';
|
||||
import startDictation from '/imports/api/captions/server/methods/startDictation';
|
||||
import stopDictation from '/imports/api/captions/server/methods/stopDictation';
|
||||
import pushSpeechTranscript from '/imports/api/captions/server/methods/pushSpeechTranscript';
|
||||
|
||||
Meteor.methods({
|
||||
updateCaptionsOwner,
|
||||
startDictation,
|
||||
stopDictation,
|
||||
pushSpeechTranscript,
|
||||
});
|
@ -1,36 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Captions from '/imports/api/captions';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import setTranscript from '/imports/api/captions/server/modifiers/setTranscript';
|
||||
import updatePad from '/imports/api/pads/server/methods/updatePad';
|
||||
|
||||
export default async function pushSpeechTranscript(locale, transcript, type) {
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(locale, String);
|
||||
check(transcript, String);
|
||||
check(type, String);
|
||||
|
||||
const captions = await Captions.findOneAsync({
|
||||
meetingId,
|
||||
ownerId: requesterUserId,
|
||||
locale,
|
||||
dictating: true,
|
||||
});
|
||||
|
||||
if (captions) {
|
||||
if (type === 'final') {
|
||||
const text = `\n${transcript}`;
|
||||
updatePad(meetingId, requesterUserId, locale, text);
|
||||
}
|
||||
|
||||
await setTranscript(meetingId, locale, transcript);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method pushSpeechTranscript ${err.stack}`);
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Captions from '/imports/api/captions';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import setDictation from '/imports/api/captions/server/modifiers/setDictation';
|
||||
|
||||
export default async function startDictation(locale) {
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(locale, String);
|
||||
|
||||
const captions = await Captions.findOneAsync({
|
||||
meetingId,
|
||||
ownerId: requesterUserId,
|
||||
locale,
|
||||
});
|
||||
|
||||
if (captions) await setDictation(meetingId, locale, true);
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method startDictation ${err.stack}`);
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Captions from '/imports/api/captions';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import setDictation from '/imports/api/captions/server/modifiers/setDictation';
|
||||
|
||||
export default async function stopDictation(locale) {
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(locale, String);
|
||||
|
||||
const captions = await Captions.findOne({
|
||||
meetingId,
|
||||
ownerId: requesterUserId,
|
||||
locale,
|
||||
});
|
||||
|
||||
if (captions) await setDictation(meetingId, locale, false);
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method stopDictation ${err.stack}`);
|
||||
}
|
||||
}
|
@ -1,30 +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 updateCaptionsOwner(locale, name) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'UpdateCaptionOwnerPubMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(locale, String);
|
||||
check(name, String);
|
||||
|
||||
const payload = {
|
||||
ownerId: requesterUserId,
|
||||
locale,
|
||||
name,
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method updateCaptionsOwner ${err.stack}`);
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import Captions from '/imports/api/captions';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default async function clearCaptions(meetingId) {
|
||||
if (meetingId) {
|
||||
try {
|
||||
const numberAffected = await Captions.removeAsync({ meetingId });
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.info(`Cleared Captions (${meetingId})`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Error on clearing captions (${meetingId}). ${err}`);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const numberAffected = await Captions.removeAsync({});
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.info('Cleared Captions (all)');
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Error on clearing captions (all). ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Captions from '/imports/api/captions';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default async function createCaptions(meetingId, locale, name) {
|
||||
try {
|
||||
check(meetingId, String);
|
||||
check(locale, String);
|
||||
check(name, String);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
locale,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
meetingId,
|
||||
locale,
|
||||
name,
|
||||
ownerId: '',
|
||||
dictating: false,
|
||||
transcript: '',
|
||||
};
|
||||
|
||||
const { numberAffected } = await Captions.upsertAsync(selector, modifier);
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.verbose(`Created captions=${locale} meeting=${meetingId}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Creating captions owner to the collection: ${err}`);
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Captions from '/imports/api/captions';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default async function setDictation(meetingId, locale, dictating) {
|
||||
try {
|
||||
check(meetingId, String);
|
||||
check(locale, String);
|
||||
check(dictating, Boolean);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
locale,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
dictating,
|
||||
transcript: '',
|
||||
},
|
||||
};
|
||||
|
||||
const { numberAffected } = Captions.upsertAsync(selector, modifier);
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.info(`Set captions=${locale} dictating=${dictating} meeting=${meetingId}`);
|
||||
} else {
|
||||
Logger.info(`Upserted captions=${locale} dictating=${dictating} meeting=${meetingId}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Setting captions dictation to the collection: ${err}`);
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Captions from '/imports/api/captions';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default async function setTranscript(meetingId, locale, transcript) {
|
||||
try {
|
||||
check(meetingId, String);
|
||||
check(locale, String);
|
||||
check(transcript, String);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
locale,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
transcript,
|
||||
},
|
||||
};
|
||||
|
||||
const numberAffected = await Captions.upsertAsync(selector, modifier);
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.debug(`Set captions=${locale} transcript=${transcript} meeting=${meetingId}`);
|
||||
} else {
|
||||
Logger.debug(`Upserted captions=${locale} transcript=${transcript} meeting=${meetingId}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Setting captions transcript to the collection: ${err}`);
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Captions from '/imports/api/captions';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default async function updateCaptionsOwner(meetingId, locale, ownerId) {
|
||||
try {
|
||||
check(meetingId, String);
|
||||
check(locale, String);
|
||||
check(ownerId, String);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
locale,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
ownerId,
|
||||
dictating: false, // Refresh dictation mode
|
||||
},
|
||||
};
|
||||
|
||||
const numberAffected = await Captions.upsert(selector, modifier);
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.info(`Added captions=${locale} owner=${ownerId} meeting=${meetingId}`);
|
||||
} else {
|
||||
Logger.info(`Upserted captions=${locale} owner=${ownerId} meeting=${meetingId}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Adding captions owner to the collection: ${err}`);
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import Captions from '/imports/api/captions';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
|
||||
|
||||
async function captions() {
|
||||
const tokenValidation = await AuthTokenValidation
|
||||
.findOneAsync({ connectionId: this.connection.id });
|
||||
|
||||
if (!tokenValidation || tokenValidation.validationStatus !== ValidationStates.VALIDATED) {
|
||||
Logger.warn(`Publishing Captions was requested by unauth connection ${this.connection.id}`);
|
||||
return Captions.find({ meetingId: '' });
|
||||
}
|
||||
|
||||
const { meetingId, userId } = tokenValidation;
|
||||
Logger.debug('Publishing Captions', { meetingId, requestedBy: userId });
|
||||
|
||||
return Captions.find({ meetingId });
|
||||
}
|
||||
|
||||
function publish(...args) {
|
||||
const boundCaptions = captions.bind(this);
|
||||
return boundCaptions(...args);
|
||||
}
|
||||
|
||||
Meteor.publish('captions', publish);
|
@ -11,7 +11,6 @@ import Meetings, {
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { initPads } from '/imports/api/pads/server/helpers';
|
||||
import createTimer from '/imports/api/timer/server/methods/createTimer';
|
||||
import { initCaptions } from '/imports/api/captions/server/helpers';
|
||||
import { addExternalVideoStreamer } from '/imports/api/external-videos/server/streamer';
|
||||
import addUserReactionsObserver from '/imports/api/user-reaction/server/helpers';
|
||||
import { LAYOUT_TYPE } from '/imports/ui/components/layout/enums';
|
||||
@ -249,9 +248,6 @@ export default async function addMeeting(meeting) {
|
||||
if (newMeeting.meetingProp.disabledFeatures.indexOf('sharedNotes') === -1) {
|
||||
initPads(meetingId);
|
||||
}
|
||||
if (newMeeting.meetingProp.disabledFeatures.indexOf('captions') === -1) {
|
||||
await initCaptions(meetingId);
|
||||
}
|
||||
if (newMeeting.meetingProp.disabledFeatures.indexOf('reactions') === -1) {
|
||||
await addUserReactionsObserver(meetingId);
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import { removeExternalVideoStreamer } from '/imports/api/external-videos/server
|
||||
import clearUsers from '/imports/api/users/server/modifiers/clearUsers';
|
||||
import clearUsersSettings from '/imports/api/users-settings/server/modifiers/clearUsersSettings';
|
||||
import clearBreakouts from '/imports/api/breakouts/server/modifiers/clearBreakouts';
|
||||
import clearCaptions from '/imports/api/captions/server/modifiers/clearCaptions';
|
||||
import clearPads from '/imports/api/pads/server/modifiers/clearPads';
|
||||
import clearVoiceUsers from '/imports/api/voice-users/server/modifiers/clearVoiceUsers';
|
||||
import clearUserInfo from '/imports/api/users-infos/server/modifiers/clearUserInfo';
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import validateAuthToken from './methods/validateAuthToken';
|
||||
import setUserEffectiveConnectionType from './methods/setUserEffectiveConnectionType';
|
||||
import userActivitySign from './methods/userActivitySign';
|
||||
import validateConnection from './methods/validateConnection';
|
||||
|
||||
Meteor.methods({
|
||||
validateConnection,
|
||||
validateAuthToken,
|
||||
setUserEffectiveConnectionType,
|
||||
userActivitySign,
|
||||
});
|
||||
|
@ -1,78 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import upsertValidationState from '/imports/api/auth-token-validation/server/modifiers/upsertValidationState';
|
||||
import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
|
||||
import pendingAuthenticationsStore from '../store/pendingAuthentications';
|
||||
|
||||
const AUTH_TIMEOUT = 120000;
|
||||
|
||||
async function validateAuthToken(meetingId, requesterUserId, requesterToken, externalId) {
|
||||
let setTimeoutRef = null;
|
||||
const userValidation = await new Promise(async (res, rej) => {
|
||||
const observeFunc = (obj) => {
|
||||
if (obj.validationStatus === ValidationStates.VALIDATED) {
|
||||
Meteor.clearTimeout(setTimeoutRef);
|
||||
return res(obj);
|
||||
}
|
||||
if (obj.validationStatus === ValidationStates.INVALID) {
|
||||
Meteor.clearTimeout(setTimeoutRef);
|
||||
return res(obj);
|
||||
}
|
||||
};
|
||||
const authTokenValidationObserver = AuthTokenValidation.find({
|
||||
connectionId: this.connection.id,
|
||||
}).observe({
|
||||
added: observeFunc,
|
||||
changed: observeFunc,
|
||||
});
|
||||
|
||||
setTimeoutRef = Meteor.setTimeout(() => {
|
||||
authTokenValidationObserver.stop();
|
||||
rej();
|
||||
}, AUTH_TIMEOUT);
|
||||
|
||||
try {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'ValidateAuthTokenReqMsg';
|
||||
|
||||
Logger.debug('ValidateAuthToken method called', { meetingId, requesterUserId, requesterToken, externalId });
|
||||
|
||||
if (!meetingId) return false;
|
||||
|
||||
// Store reference of methodInvocationObject ( to postpone the connection userId definition )
|
||||
pendingAuthenticationsStore.add(meetingId, requesterUserId, requesterToken, this);
|
||||
await upsertValidationState(
|
||||
meetingId,
|
||||
requesterUserId,
|
||||
ValidationStates.VALIDATING,
|
||||
this.connection.id,
|
||||
);
|
||||
|
||||
const payload = {
|
||||
userId: requesterUserId,
|
||||
authToken: requesterToken,
|
||||
};
|
||||
|
||||
Logger.info(`User '${requesterUserId}' is trying to validate auth token for meeting '${meetingId}' from connection '${this.connection.id}'`);
|
||||
|
||||
return RedisPubSub.publishUserMessage(
|
||||
CHANNEL,
|
||||
EVENT_NAME,
|
||||
meetingId,
|
||||
requesterUserId,
|
||||
payload,
|
||||
);
|
||||
} catch (err) {
|
||||
const errMsg = `Exception while invoking method validateAuthToken ${err}`;
|
||||
Logger.error(errMsg);
|
||||
rej(errMsg);
|
||||
Meteor.clearTimeout(setTimeoutRef);
|
||||
authTokenValidationObserver.stop();
|
||||
}
|
||||
});
|
||||
return userValidation;
|
||||
}
|
||||
|
||||
export default validateAuthToken;
|
@ -12,7 +12,7 @@ export default async function handleWebcamSync({ body }, meetingId) {
|
||||
|
||||
const streamsIds = webcamListSync.map((webcam) => webcam.stream);
|
||||
|
||||
const webcamStreams = VideoStreams.find({
|
||||
const webcamStreams = await VideoStreams.find({
|
||||
meetingId,
|
||||
stream: { $in: streamsIds },
|
||||
}, {
|
||||
@ -42,7 +42,7 @@ export default async function handleWebcamSync({ body }, meetingId) {
|
||||
stream: 1,
|
||||
userId: 1,
|
||||
},
|
||||
}).fetchAsynch();
|
||||
}).fetchAsync();
|
||||
|
||||
await Promise.all(videoStreamsToRemove
|
||||
.map(async (videoStream) => {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import Logger from './logger';
|
||||
import userLeaving from '/imports/api/users/server/methods/userLeaving';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import AuthTokenValidation from '/imports/api/auth-token-validation';
|
||||
import Users from '/imports/api/users';
|
||||
import { check } from 'meteor/check';
|
||||
|
||||
@ -140,13 +139,6 @@ class ClientConnections {
|
||||
|
||||
Logger.debug(`Found ${activeConnections.length} active connections in server`);
|
||||
|
||||
const onlineUsers = AuthTokenValidation
|
||||
.find(
|
||||
{ connectionId: { $in: activeConnections } },
|
||||
{ fields: { meetingId: 1, userId: 1 } }
|
||||
)
|
||||
.fetch();
|
||||
|
||||
const onlineUsersId = onlineUsers.map(({ userId }) => userId);
|
||||
|
||||
const usersQuery = { userId: { $nin: onlineUsersId } };
|
||||
|
@ -49,6 +49,29 @@ export interface Reaction {
|
||||
reactionEmoji: string;
|
||||
}
|
||||
|
||||
export interface BreakoutRooms {
|
||||
currentRoomJoined: boolean;
|
||||
assignedAt: string;
|
||||
breakoutRoomId: string;
|
||||
currentRoomIsOnline: boolean | null;
|
||||
currentRoomPriority: number;
|
||||
currentRoomRegisteredAt: string | null;
|
||||
durationInSeconds: number;
|
||||
endedAt: string | null;
|
||||
freeJoin: boolean;
|
||||
inviteDismissedAt: string | null;
|
||||
isDefaultName: boolean;
|
||||
joinURL: string;
|
||||
lastRoomIsOnline: boolean;
|
||||
lastRoomJoinedAt: string;
|
||||
lastRoomJoinedId: string;
|
||||
name: string;
|
||||
sendInvitationToModerators: boolean;
|
||||
sequence: number;
|
||||
shortName: string;
|
||||
showInvitation: boolean;
|
||||
startedAt: string;
|
||||
}
|
||||
export interface UserClientSettings {
|
||||
userClientSettingsJson: string;
|
||||
}
|
||||
@ -87,8 +110,8 @@ export interface User {
|
||||
isDialIn: boolean;
|
||||
voice?: Partial<Voice>;
|
||||
locked: boolean;
|
||||
registeredAt: number;
|
||||
registeredOn: string;
|
||||
registeredAt: string;
|
||||
registeredOn: number;
|
||||
hasDrawPermissionOnCurrentPage: boolean;
|
||||
lastBreakoutRoom?: LastBreakoutRoom;
|
||||
cameras: Array<Cameras>;
|
||||
@ -99,6 +122,7 @@ export interface User {
|
||||
away: boolean;
|
||||
raiseHand: boolean;
|
||||
reaction: Reaction;
|
||||
breakoutRooms: BreakoutRooms;
|
||||
customParameters: Array<CustomParameter>;
|
||||
userClientSettings: UserClientSettings;
|
||||
}
|
||||
|
@ -1,11 +1,9 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import CaptionsButtonContainer from '/imports/ui/components/captions/button/container';
|
||||
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/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';
|
||||
import AudioControlsContainer from '../audio/audio-graphql/audio-controls/component';
|
||||
@ -21,20 +19,11 @@ class ActionsBar extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isCaptionsReaderMenuModalOpen: false,
|
||||
};
|
||||
|
||||
this.setCaptionsReaderMenuModalIsOpen = this.setCaptionsReaderMenuModalIsOpen.bind(this);
|
||||
this.setRenderRaiseHand = this.renderRaiseHand.bind(this);
|
||||
this.actionsBarRef = React.createRef();
|
||||
this.renderPluginsActionBarItems = this.renderPluginsActionBarItems.bind(this);
|
||||
}
|
||||
|
||||
setCaptionsReaderMenuModalIsOpen(value) {
|
||||
this.setState({ isCaptionsReaderMenuModalOpen: value });
|
||||
}
|
||||
|
||||
renderPluginsActionBarItems(position) {
|
||||
const { actionBarItems } = this.props;
|
||||
return (
|
||||
@ -111,7 +100,6 @@ class ActionsBar extends PureComponent {
|
||||
stopExternalVideoShare,
|
||||
isTimerActive,
|
||||
isTimerEnabled,
|
||||
isCaptionsAvailable,
|
||||
isMeteorConnected,
|
||||
isPollingEnabled,
|
||||
isRaiseHandButtonCentered,
|
||||
@ -125,8 +113,6 @@ class ActionsBar extends PureComponent {
|
||||
setPresentationFitToWidth,
|
||||
} = this.props;
|
||||
|
||||
const { isCaptionsReaderMenuModalOpen } = this.state;
|
||||
|
||||
const { selectedLayout } = Settings.application;
|
||||
const shouldShowPresentationButton = selectedLayout !== LAYOUT_TYPE.CAMERAS_ONLY
|
||||
&& selectedLayout !== LAYOUT_TYPE.PARTICIPANTS_AND_CHAT_ONLY;
|
||||
@ -165,29 +151,6 @@ class ActionsBar extends PureComponent {
|
||||
setPresentationFitToWidth,
|
||||
}}
|
||||
/>
|
||||
{isCaptionsAvailable
|
||||
? (
|
||||
<>
|
||||
<CaptionsButtonContainer {...{
|
||||
intl,
|
||||
setIsOpen: this.setCaptionsReaderMenuModalIsOpen,
|
||||
}}
|
||||
/>
|
||||
{
|
||||
isCaptionsReaderMenuModalOpen ? (
|
||||
<CaptionsReaderMenuContainer
|
||||
{...{
|
||||
onRequestClose: () => this.setCaptionsReaderMenuModalIsOpen(false),
|
||||
priority: 'low',
|
||||
setIsOpen: this.setCaptionsReaderMenuModalIsOpen,
|
||||
isOpen: isCaptionsReaderMenuModalOpen,
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
</>
|
||||
)
|
||||
: null}
|
||||
{!deviceInfo.isMobile
|
||||
? (
|
||||
<AudioCaptionsButtonContainer />
|
||||
|
@ -7,7 +7,6 @@ import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import ActionsBar from './component';
|
||||
import Service from './service';
|
||||
import CaptionsService from '/imports/ui/components/captions/service';
|
||||
import TimerService from '/imports/ui/components/timer/service';
|
||||
import { layoutSelectOutput, layoutDispatch } from '../layout/context';
|
||||
import { isExternalVideoEnabled, isPollingEnabled, isPresentationEnabled } from '/imports/ui/services/features';
|
||||
@ -93,7 +92,6 @@ export default withTracker(() => ({
|
||||
isSharedNotesPinned: Service.isSharedNotesPinned(),
|
||||
hasScreenshare: isScreenBroadcasting(),
|
||||
hasCameraAsContent: isCameraAsContentBroadcasting(),
|
||||
isCaptionsAvailable: CaptionsService.isCaptionsAvailable(),
|
||||
isTimerActive: TimerService.isActive(),
|
||||
isTimerEnabled: TimerService.isEnabled(),
|
||||
isMeteorConnected: Meteor.status().connected,
|
||||
|
@ -4,7 +4,6 @@ import { makeCall } from '/imports/ui/services/api';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Breakouts from '/imports/api/breakouts';
|
||||
import NotesService from '/imports/ui/components/notes/service';
|
||||
import BreakoutsHistory from '/imports/api/breakouts-history';
|
||||
|
||||
const DIAL_IN_USER = 'dial-in-user';
|
||||
|
||||
@ -12,16 +11,6 @@ const getBreakouts = () => Breakouts.find({ parentMeetingId: Auth.meetingID })
|
||||
.fetch()
|
||||
.sort((a, b) => a.sequence - b.sequence);
|
||||
|
||||
const getLastBreakouts = () => {
|
||||
const lastBreakouts = BreakoutsHistory.findOne({ meetingId: Auth.meetingID });
|
||||
if (lastBreakouts) {
|
||||
return lastBreakouts.rooms
|
||||
.sort((a, b) => a.sequence - b.sequence);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const currentBreakoutUsers = (user) => !Breakouts.findOne({
|
||||
'joinedUsers.userId': new RegExp(`^${user.userId}`),
|
||||
});
|
||||
@ -50,7 +39,6 @@ export default {
|
||||
joinedUsers: { $exists: true },
|
||||
}, { fields: { joinedUsers: 1, breakoutId: 1, sequence: 1 }, sort: { sequence: 1 } }).fetch(),
|
||||
getBreakouts,
|
||||
getLastBreakouts,
|
||||
getUsersNotJoined,
|
||||
isSharedNotesPinned: () => NotesService.isSharedNotesPinned(),
|
||||
};
|
||||
|
@ -30,7 +30,8 @@ const TimeSync: React.FC = () => {
|
||||
useEffect(() => {
|
||||
if (!loading && data) {
|
||||
const time = new Date(data.current_time[0].currentTimestamp);
|
||||
setTimeSync(time.getTime() - new Date().getTime());
|
||||
const dateNow = new Date();
|
||||
setTimeSync(time.getTime() - dateNow.getTime());
|
||||
}
|
||||
}, [data, loading]);
|
||||
return null;
|
||||
|
@ -20,7 +20,6 @@ import RaiseHandNotifier from '/imports/ui/components/raisehand-notifier/contain
|
||||
import ManyWebcamsNotifier from '/imports/ui/components/video-provider/many-users-notify/container';
|
||||
import AudioCaptionsSpeechContainer from '/imports/ui/components/audio/captions/speech/container';
|
||||
import UploaderContainer from '/imports/ui/components/presentation/presentation-uploader/container';
|
||||
import CaptionsSpeechContainer from '/imports/ui/components/captions/speech/container';
|
||||
import ScreenReaderAlertContainer from '../screenreader-alert/container';
|
||||
import ScreenReaderAlertAdapter from '../screenreader-alert/adapter';
|
||||
import WebcamContainer from '../webcam/container';
|
||||
@ -41,7 +40,7 @@ import SidebarContentContainer from '../sidebar-content/container';
|
||||
import PluginsEngineManager from '../plugins-engine/manager';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import { registerTitleView } from '/imports/utils/dom-utils';
|
||||
import Notifications from '../notifications/container';
|
||||
import Notifications from '../notifications/component';
|
||||
import GlobalStyles from '/imports/ui/stylesheets/styled-components/globalStyles';
|
||||
import ActionsBarContainer from '../actions-bar/container';
|
||||
import PushLayoutEngine from '../layout/push-layout/pushLayoutEngine';
|
||||
@ -52,6 +51,7 @@ import AppService from '/imports/ui/components/app/service';
|
||||
import TimerService from '/imports/ui/components/timer/service';
|
||||
import TimeSync from './app-graphql/time-sync/component';
|
||||
import PresentationUploaderToastContainer from '/imports/ui/components/presentation/presentation-toast/presentation-uploader-toast/container';
|
||||
import BreakoutJoinConfirmationContainerGraphQL from '../breakout-join-confirmation/breakout-join-confirmation-graphql/component';
|
||||
import FloatingWindowContainer from '/imports/ui/components/floating-window/container';
|
||||
import ChatAlertContainerGraphql from '../chat/chat-graphql/alert/component';
|
||||
|
||||
@ -130,13 +130,11 @@ const intlMessages = defineMessages({
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
captions: PropTypes.element,
|
||||
darkTheme: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
actionsbar: null,
|
||||
captions: null,
|
||||
};
|
||||
|
||||
const isLayeredView = window.matchMedia(`(max-width: ${SMALL_VIEWPORT_BREAKPOINT}px)`);
|
||||
@ -373,31 +371,6 @@ class App extends Component {
|
||||
&& (isPhone || isLayeredView.matches);
|
||||
}
|
||||
|
||||
renderCaptions() {
|
||||
const {
|
||||
captions,
|
||||
captionsStyle,
|
||||
} = this.props;
|
||||
|
||||
if (!captions) return null;
|
||||
|
||||
return (
|
||||
<Styled.CaptionsWrapper
|
||||
role="region"
|
||||
style={
|
||||
{
|
||||
position: 'absolute',
|
||||
left: captionsStyle.left,
|
||||
right: captionsStyle.right,
|
||||
maxWidth: captionsStyle.maxWidth,
|
||||
}
|
||||
}
|
||||
>
|
||||
{captions}
|
||||
</Styled.CaptionsWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
renderAudioCaptions() {
|
||||
const {
|
||||
audioCaptions,
|
||||
@ -585,7 +558,7 @@ setRandomUserSelectModalIsOpen(value) {
|
||||
pushAlertEnabled,
|
||||
shouldShowPresentation,
|
||||
shouldShowScreenshare,
|
||||
shouldShowSharedNotes,
|
||||
isSharedNotesPinned,
|
||||
isPresenter,
|
||||
selectedLayout,
|
||||
presentationIsOpen,
|
||||
@ -623,7 +596,7 @@ setRandomUserSelectModalIsOpen(value) {
|
||||
<BannerBarContainer />
|
||||
<NotificationsBarContainer />
|
||||
<SidebarNavigationContainer />
|
||||
<SidebarContentContainer isSharedNotesPinned={shouldShowSharedNotes} />
|
||||
<SidebarContentContainer isSharedNotesPinned={isSharedNotesPinned} />
|
||||
<NavBarContainer main="new" />
|
||||
<WebcamContainer isLayoutSwapped={!presentationIsOpen} layoutType={selectedLayout} />
|
||||
<ExternalVideoPlayerContainer />
|
||||
@ -653,20 +626,17 @@ setRandomUserSelectModalIsOpen(value) {
|
||||
)
|
||||
: null
|
||||
}
|
||||
{shouldShowSharedNotes
|
||||
{isSharedNotesPinned
|
||||
? (
|
||||
<NotesContainer
|
||||
area="media"
|
||||
layoutType={selectedLayout}
|
||||
/>
|
||||
) : null}
|
||||
{this.renderCaptions()}
|
||||
<AudioCaptionsSpeechContainer />
|
||||
{this.renderAudioCaptions()}
|
||||
<PresentationUploaderToastContainer intl={intl} />
|
||||
<UploaderContainer />
|
||||
<CaptionsSpeechContainer isModerator={isModerator} />
|
||||
<BreakoutRoomInvitation isModerator={isModerator} />
|
||||
<BreakoutJoinConfirmationContainerGraphQL />
|
||||
<AudioContainer {...{
|
||||
isAudioModalOpen,
|
||||
setAudioModalIsOpen: this.setAudioModalIsOpen,
|
||||
|
@ -1,20 +1,16 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
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 Meetings from '/imports/api/meetings';
|
||||
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';
|
||||
import CaptionsService from '/imports/ui/components/captions/service';
|
||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import UserInfos from '/imports/api/users-infos';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import MediaService from '/imports/ui/components/media/service';
|
||||
import { isPresentationEnabled, isExternalVideoEnabled } from '/imports/ui/services/features';
|
||||
import { isEqual } from 'radash';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
||||
import { LAYOUT_TYPE } from '/imports/ui/components/layout/enums';
|
||||
@ -68,7 +64,6 @@ const AppContainer = (props) => {
|
||||
meetingLayoutCameraPosition,
|
||||
meetingLayoutFocusedCamera,
|
||||
meetingLayoutVideoRate,
|
||||
isSharedNotesPinned,
|
||||
viewScreenshare,
|
||||
...otherProps
|
||||
} = props;
|
||||
@ -81,6 +76,7 @@ const AppContainer = (props) => {
|
||||
const cameraDock = layoutSelectOutput((i) => i.cameraDock);
|
||||
const cameraDockInput = layoutSelectInput((i) => i.cameraDock);
|
||||
const presentation = layoutSelectInput((i) => i.presentation);
|
||||
const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
|
||||
const deviceType = layoutSelect((i) => i.deviceType);
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
|
||||
@ -90,8 +86,9 @@ const AppContainer = (props) => {
|
||||
const toggleVoice = useToggleVoice();
|
||||
const setLocalSettings = useUserChangedLocalSettings();
|
||||
const { data: pinnedPadData } = useSubscription(PINNED_PAD_SUBSCRIPTION);
|
||||
const shouldShowSharedNotes = !!pinnedPadData
|
||||
const isSharedNotesPinnedFromGraphql = !!pinnedPadData
|
||||
&& pinnedPadData.sharedNotes[0]?.sharedNotesExtId === NOTES_CONFIG.id;
|
||||
const isSharedNotesPinned = sharedNotesInput?.isPinned && isSharedNotesPinnedFromGraphql;
|
||||
|
||||
const setMobileUser = (mobile) => {
|
||||
setMobileFlag({
|
||||
@ -185,7 +182,7 @@ const AppContainer = (props) => {
|
||||
|
||||
const shouldShowScreenshare = propsShouldShowScreenshare
|
||||
&& (viewScreenshare || isPresenter);
|
||||
const shouldShowPresentation = (!shouldShowScreenshare && !shouldShowSharedNotes
|
||||
const shouldShowPresentation = (!shouldShowScreenshare && !isSharedNotesPinned
|
||||
&& !shouldShowExternalVideo && !shouldShowGenericComponent
|
||||
&& (presentationIsOpen || presentationRestoreOnUpdate)) && isPresentationEnabled();
|
||||
return currentUserId
|
||||
@ -227,7 +224,7 @@ const AppContainer = (props) => {
|
||||
enforceLayout: validateEnforceLayout(currentUserData),
|
||||
isModerator,
|
||||
shouldShowScreenshare,
|
||||
shouldShowSharedNotes,
|
||||
isSharedNotesPinned,
|
||||
shouldShowPresentation,
|
||||
setMobileUser,
|
||||
toggleVoice,
|
||||
@ -297,8 +294,7 @@ export default withTracker(() => {
|
||||
const LAYOUT_CONFIG = window.meetingClientSettings.public.layout;
|
||||
|
||||
return {
|
||||
captions: CaptionsService.isCaptionsActive() ? <CaptionsContainer /> : null,
|
||||
audioCaptions: AudioCaptionsService.getAudioCaptions() ? <AudioCaptionsLiveContainer /> : null,
|
||||
audioCaptions: <AudioCaptionsLiveContainer />,
|
||||
fontSize: getFontSize(),
|
||||
hasBreakoutRooms: getBreakoutRooms().length > 0,
|
||||
customStyle: getFromUserSettings('bbb_custom_style', false),
|
||||
@ -335,7 +331,6 @@ export default withTracker(() => {
|
||||
hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false),
|
||||
hideNavBar: getFromUserSettings('bbb_hide_nav_bar', false),
|
||||
ignorePollNotifications: Session.get('ignorePollNotifications'),
|
||||
isSharedNotesPinned: MediaService.shouldShowSharedNotes(),
|
||||
User: currentUser,
|
||||
};
|
||||
})(AppContainer);
|
||||
|
@ -1,99 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Session } from 'meteor/session';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import LoadingScreen from '/imports/ui/components/common/loading-screen/component';
|
||||
|
||||
const STATUS_CONNECTING = 'connecting';
|
||||
|
||||
class AuthenticatedHandler extends Component {
|
||||
static setError({ description, error }) {
|
||||
if (error) Session.set('codeError', error);
|
||||
Session.set('errorMessageDescription', description);
|
||||
}
|
||||
|
||||
static shouldAuthenticate(status, lastStatus) {
|
||||
return lastStatus != null && lastStatus === STATUS_CONNECTING && status.connected;
|
||||
}
|
||||
|
||||
static updateStatus(status, lastStatus) {
|
||||
return lastStatus !== STATUS_CONNECTING ? status.status : lastStatus;
|
||||
}
|
||||
|
||||
static addReconnectObservable() {
|
||||
let lastStatus = null;
|
||||
|
||||
Tracker.autorun(() => {
|
||||
lastStatus = AuthenticatedHandler.updateStatus(Meteor.status(), lastStatus);
|
||||
|
||||
if (AuthenticatedHandler.shouldAuthenticate(Meteor.status(), lastStatus)) {
|
||||
Session.set('userWillAuth', true);
|
||||
Auth.authenticate(true);
|
||||
lastStatus = Meteor.status().status;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async authenticatedRouteHandler(callback) {
|
||||
if (Auth.loggedIn) {
|
||||
callback();
|
||||
}
|
||||
AuthenticatedHandler.addReconnectObservable();
|
||||
|
||||
const setReason = (reason) => {
|
||||
const log = reason.error === 403 ? 'warn' : 'error';
|
||||
logger[log]({
|
||||
logCode: 'authenticatedhandlercomponent_setreason',
|
||||
extraInfo: { reason },
|
||||
}, 'Encountered error while trying to authenticate');
|
||||
|
||||
AuthenticatedHandler.setError(reason);
|
||||
callback();
|
||||
};
|
||||
|
||||
try {
|
||||
const getAuthenticate = await Auth.authenticate();
|
||||
callback(getAuthenticate);
|
||||
} catch (error) {
|
||||
setReason(error);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
authenticated: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (Session.get('codeError')) {
|
||||
console.log('Session.get(codeError)', Session.get('codeError'));
|
||||
this.setState({ authenticated: true });
|
||||
}
|
||||
AuthenticatedHandler.authenticatedRouteHandler((value, error) => {
|
||||
if (error) AuthenticatedHandler.setError(error);
|
||||
this.setState({ authenticated: true });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
} = this.props;
|
||||
const {
|
||||
authenticated,
|
||||
} = this.state;
|
||||
|
||||
Session.set('isMeetingEnded', false);
|
||||
Session.set('isPollOpen', false);
|
||||
// TODO: breakoutRoomIsOpen doesn't seem used
|
||||
Session.set('breakoutRoomIsOpen', false);
|
||||
|
||||
return authenticated
|
||||
? children
|
||||
: (<LoadingScreen />);
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthenticatedHandler;
|
@ -0,0 +1,201 @@
|
||||
import { useMutation, useSubscription } from '@apollo/client';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import Styled from './styles';
|
||||
import ModalFullscreen from '/imports/ui/components/common/modal/fullscreen/component';
|
||||
import {
|
||||
BreakoutRoom,
|
||||
getBreakoutCount,
|
||||
GetBreakoutCountResponse,
|
||||
getBreakoutData,
|
||||
GetBreakoutDataResponse,
|
||||
handleinviteDismissedAt,
|
||||
} from './queries';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import { BREAKOUT_ROOM_REQUEST_JOIN_URL } from '../../breakout-room/mutations';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
title: {
|
||||
id: 'app.breakoutJoinConfirmation.title',
|
||||
description: 'Join breakout room title',
|
||||
},
|
||||
message: {
|
||||
id: 'app.breakoutJoinConfirmation.message',
|
||||
description: 'Join breakout confirm message',
|
||||
},
|
||||
freeJoinMessage: {
|
||||
id: 'app.breakoutJoinConfirmation.freeJoinMessage',
|
||||
description: 'Join breakout confirm message',
|
||||
},
|
||||
confirmLabel: {
|
||||
id: 'app.createBreakoutRoom.join',
|
||||
description: 'Join confirmation button label',
|
||||
},
|
||||
confirmDesc: {
|
||||
id: 'app.breakoutJoinConfirmation.confirmDesc',
|
||||
description: 'adds context to confirm option',
|
||||
},
|
||||
dismissLabel: {
|
||||
id: 'app.breakoutJoinConfirmation.dismissLabel',
|
||||
description: 'Cancel button label',
|
||||
},
|
||||
dismissDesc: {
|
||||
id: 'app.breakoutJoinConfirmation.dismissDesc',
|
||||
description: 'adds context to dismiss option',
|
||||
},
|
||||
generatingURL: {
|
||||
id: 'app.createBreakoutRoom.generatingURLMessage',
|
||||
description: 'label for generating breakout room url',
|
||||
},
|
||||
});
|
||||
|
||||
interface BreakoutJoinConfirmationProps {
|
||||
freeJoin: boolean;
|
||||
breakouts: BreakoutRoom[];
|
||||
currentUserJoined: boolean,
|
||||
}
|
||||
|
||||
const BreakoutJoinConfirmation: React.FC<BreakoutJoinConfirmationProps> = ({
|
||||
freeJoin,
|
||||
breakouts,
|
||||
currentUserJoined,
|
||||
}) => {
|
||||
const [breakoutRoomRequestJoinURL] = useMutation(BREAKOUT_ROOM_REQUEST_JOIN_URL);
|
||||
const [callHandleinviteDismissedAt] = useMutation(handleinviteDismissedAt);
|
||||
|
||||
const intl = useIntl();
|
||||
const [waiting, setWaiting] = React.useState(false);
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [selectValue, setSelectValue] = React.useState('');
|
||||
|
||||
const requestJoinURL = (breakoutRoomId: string) => {
|
||||
breakoutRoomRequestJoinURL({ variables: { breakoutRoomId } });
|
||||
};
|
||||
const handleSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSelectValue(event.target.value);
|
||||
const selectedBreakout = breakouts.find(({ breakoutRoomId }) => breakoutRoomId === event.target.value);
|
||||
if (!selectedBreakout?.joinURL) {
|
||||
requestJoinURL(event.target.value);
|
||||
setWaiting(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleJoinBreakoutConfirmation = () => {
|
||||
if (breakouts.length === 1) {
|
||||
const breakout = breakouts[0];
|
||||
|
||||
if (breakout?.joinURL) {
|
||||
window.open(breakout.joinURL, '_blank');
|
||||
}
|
||||
setIsOpen(false);
|
||||
} else {
|
||||
const selectedBreakout = breakouts.find(({ breakoutRoomId }) => breakoutRoomId === selectValue);
|
||||
if (selectedBreakout?.joinURL) {
|
||||
window.open(selectedBreakout.joinURL, '_blank');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const select = useMemo(() => {
|
||||
return (
|
||||
<Styled.SelectParent>
|
||||
{`${intl.formatMessage(intlMessages.freeJoinMessage)}`}
|
||||
<Styled.Select
|
||||
value={selectValue}
|
||||
onChange={handleSelectChange}
|
||||
disabled={waiting}
|
||||
data-test="selectBreakoutRoomBtn"
|
||||
>
|
||||
{
|
||||
breakouts.sort((a, b) => a.sequence - b.sequence).map(({ shortName, breakoutRoomId }) => (
|
||||
<option
|
||||
data-test="roomOption"
|
||||
key={breakoutRoomId}
|
||||
value={breakoutRoomId}
|
||||
>
|
||||
{shortName}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</Styled.Select>
|
||||
{ waiting ? <span data-test="labelGeneratingURL">{intl.formatMessage(intlMessages.generatingURL)}</span> : null}
|
||||
</Styled.SelectParent>
|
||||
);
|
||||
}, [breakouts, waiting, selectValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (waiting) {
|
||||
const breakout = breakouts.find(({ breakoutRoomId }) => breakoutRoomId === selectValue);
|
||||
if (breakout?.joinURL) {
|
||||
setWaiting(false);
|
||||
}
|
||||
}
|
||||
}, [breakouts, waiting]);
|
||||
|
||||
useEffect(() => {
|
||||
if (breakouts?.length > 0 && !currentUserJoined) {
|
||||
setIsOpen(true);
|
||||
}
|
||||
}, [breakouts, currentUserJoined]);
|
||||
|
||||
return (
|
||||
<ModalFullscreen
|
||||
title={intl.formatMessage(intlMessages.title)}
|
||||
confirm={{
|
||||
callback: handleJoinBreakoutConfirmation,
|
||||
label: intl.formatMessage(intlMessages.confirmLabel),
|
||||
description: intl.formatMessage(intlMessages.confirmDesc),
|
||||
icon: 'popout_window',
|
||||
disabled: waiting,
|
||||
}}
|
||||
dismiss={{
|
||||
callback: () => {
|
||||
setIsOpen(false);
|
||||
callHandleinviteDismissedAt();
|
||||
},
|
||||
label: intl.formatMessage(intlMessages.dismissLabel),
|
||||
description: intl.formatMessage(intlMessages.dismissDesc),
|
||||
}}
|
||||
{...{
|
||||
setIsOpen,
|
||||
isOpen,
|
||||
priority: 'medium',
|
||||
}}
|
||||
>
|
||||
{freeJoin ? select : `${intl.formatMessage(intlMessages.message)} ${breakouts[0].shortName}?`}
|
||||
</ModalFullscreen>
|
||||
);
|
||||
};
|
||||
|
||||
const BreakoutJoinConfirmationContainer: React.FC = () => {
|
||||
const { data: currentUser } = useCurrentUser((u) => {
|
||||
return {
|
||||
isModerator: u.isModerator,
|
||||
breakoutRooms: u.breakoutRooms,
|
||||
};
|
||||
});
|
||||
const {
|
||||
data: breakoutData,
|
||||
} = useSubscription<GetBreakoutDataResponse>(getBreakoutData);
|
||||
|
||||
const {
|
||||
data: breakoutCountData,
|
||||
} = useSubscription<GetBreakoutCountResponse>(getBreakoutCount);
|
||||
if (!breakoutCountData || !breakoutCountData.breakoutRoom_aggregate.aggregate.count) return null;
|
||||
if (!breakoutData || breakoutData.breakoutRoom.length === 0) return null;
|
||||
const firstBreakout = breakoutData.breakoutRoom[0];
|
||||
const {
|
||||
freeJoin,
|
||||
sendInvitationToModerators,
|
||||
} = firstBreakout;
|
||||
if (!sendInvitationToModerators && currentUser?.isModerator) return null;
|
||||
return (
|
||||
<BreakoutJoinConfirmation
|
||||
freeJoin={freeJoin}
|
||||
breakouts={breakoutData.breakoutRoom}
|
||||
currentUserJoined={currentUser?.breakoutRooms?.currentRoomJoined ?? false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BreakoutJoinConfirmationContainer;
|
@ -0,0 +1,62 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export interface BreakoutRoom {
|
||||
freeJoin: boolean;
|
||||
shortName: string;
|
||||
sendInvitationToModerators: boolean;
|
||||
sequence: number;
|
||||
showInvitation: boolean;
|
||||
joinURL: string | null;
|
||||
breakoutRoomId: string;
|
||||
}
|
||||
|
||||
export interface GetBreakoutDataResponse {
|
||||
breakoutRoom: BreakoutRoom[];
|
||||
}
|
||||
|
||||
export interface BreakoutRoomAggregate {
|
||||
aggregate: {
|
||||
count: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GetBreakoutCountResponse {
|
||||
breakoutRoom_aggregate: BreakoutRoomAggregate;
|
||||
}
|
||||
|
||||
export const handleinviteDismissedAt = gql`
|
||||
mutation {
|
||||
update_breakoutRoom_user(where: {}, _set: {inviteDismissedAt: "now()"}) {
|
||||
affected_rows
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const getBreakoutCount = gql`
|
||||
subscription getBreakoutCount {
|
||||
breakoutRoom_aggregate (where: {showInvitation: {_eq: true}}) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const getBreakoutData = gql`
|
||||
subscription getBreakoutData {
|
||||
breakoutRoom {
|
||||
freeJoin
|
||||
shortName
|
||||
sendInvitationToModerators
|
||||
sequence
|
||||
showInvitation
|
||||
joinURL
|
||||
breakoutRoomId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
getBreakoutCount,
|
||||
getBreakoutData,
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
import styled from 'styled-components';
|
||||
import { colorWhite, colorGrayLighter } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
|
||||
const SelectParent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Select = styled.select`
|
||||
background-color: ${colorWhite};
|
||||
width: 50%;
|
||||
margin: 1rem;
|
||||
border-color: ${colorGrayLighter};
|
||||
`;
|
||||
|
||||
export default {
|
||||
SelectParent,
|
||||
Select,
|
||||
};
|
@ -221,7 +221,7 @@ class BreakoutJoinConfirmation extends Component {
|
||||
label: intl.formatMessage(intlMessages.dismissLabel),
|
||||
description: intl.formatMessage(intlMessages.dismissDesc),
|
||||
}}
|
||||
{...{
|
||||
{...{
|
||||
setIsOpen,
|
||||
isOpen,
|
||||
priority,
|
||||
|
@ -0,0 +1,130 @@
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import CreateBreakoutRoomContainerGraphql from '/imports/ui/components/breakout-room/breakout-room-graphql/create-breakout-room/component';
|
||||
import Trigger from '/imports/ui/components/common/control-header/right/component';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
options: {
|
||||
id: 'app.breakout.dropdown.options',
|
||||
description: 'Breakout options label',
|
||||
},
|
||||
manageDuration: {
|
||||
id: 'app.breakout.dropdown.manageDuration',
|
||||
description: 'Manage duration label',
|
||||
},
|
||||
manageUsers: {
|
||||
id: 'app.breakout.dropdown.manageUsers',
|
||||
description: 'Manage users label',
|
||||
},
|
||||
destroy: {
|
||||
id: 'app.breakout.dropdown.destroyAll',
|
||||
description: 'Destroy breakouts label',
|
||||
},
|
||||
});
|
||||
|
||||
interface BreakoutDropdownProps {
|
||||
openBreakoutTimeManager: () => void;
|
||||
endAllBreakouts: () => void;
|
||||
isMeteorConnected: boolean;
|
||||
amIModerator: boolean;
|
||||
isRTL: boolean;
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
key: string;
|
||||
dataTest: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const BreakoutDropdown: React.FC<BreakoutDropdownProps> = ({
|
||||
openBreakoutTimeManager,
|
||||
endAllBreakouts,
|
||||
isMeteorConnected,
|
||||
amIModerator,
|
||||
isRTL,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [isCreateBreakoutRoomModalOpen, setIsCreateBreakoutRoomModalOpen] = useState(false);
|
||||
|
||||
const getAvailableActions = (): MenuItem[] => {
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
key: 'breakoutTimeManager',
|
||||
dataTest: 'openBreakoutTimeManager',
|
||||
label: intl.formatMessage(intlMessages.manageDuration),
|
||||
onClick: () => {
|
||||
openBreakoutTimeManager();
|
||||
},
|
||||
});
|
||||
|
||||
menuItems.push({
|
||||
key: 'updateBreakoutUsers',
|
||||
dataTest: 'openUpdateBreakoutUsersModal',
|
||||
label: intl.formatMessage(intlMessages.manageUsers),
|
||||
onClick: () => {
|
||||
setIsCreateBreakoutRoomModalOpen(true);
|
||||
},
|
||||
});
|
||||
|
||||
if (amIModerator) {
|
||||
menuItems.push({
|
||||
key: 'endAllBreakouts',
|
||||
dataTest: 'endAllBreakouts',
|
||||
label: intl.formatMessage(intlMessages.destroy),
|
||||
disabled: !isMeteorConnected,
|
||||
onClick: () => {
|
||||
endAllBreakouts();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return menuItems;
|
||||
};
|
||||
|
||||
const setCreateBreakoutRoomModalIsOpen = (value: boolean) => {
|
||||
setIsCreateBreakoutRoomModalOpen(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<BBBMenu
|
||||
trigger={
|
||||
(
|
||||
<Trigger
|
||||
data-test="breakoutOptionsMenu"
|
||||
icon="more"
|
||||
label={intl.formatMessage(intlMessages.options)}
|
||||
aria-label={intl.formatMessage(intlMessages.options)}
|
||||
onClick={() => null}
|
||||
/>
|
||||
)
|
||||
}
|
||||
opts={{
|
||||
id: 'breakoutroom-dropdown-menu',
|
||||
keepMounted: true,
|
||||
transitionDuration: 0,
|
||||
elevation: 3,
|
||||
getcontentanchorel: null,
|
||||
fullwidth: 'true',
|
||||
anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
|
||||
transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
|
||||
}}
|
||||
actions={getAvailableActions()}
|
||||
/>
|
||||
{isCreateBreakoutRoomModalOpen ? (
|
||||
<CreateBreakoutRoomContainerGraphql
|
||||
isUpdate
|
||||
priority="low"
|
||||
setIsOpen={setCreateBreakoutRoomModalIsOpen}
|
||||
isOpen={isCreateBreakoutRoomModalOpen}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BreakoutDropdown;
|
@ -0,0 +1,382 @@
|
||||
import { useMutation, useSubscription } from '@apollo/client';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import {
|
||||
BreakoutRoom,
|
||||
GetBreakoutDataResponse,
|
||||
GetIfUserJoinedBreakoutRoomResponse,
|
||||
getBreakoutData,
|
||||
getIfUserJoinedBreakoutRoom,
|
||||
} from './queries';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import Header from '/imports/ui/components/common/control-header/component';
|
||||
import Styled from './styles';
|
||||
import { layoutDispatch, layoutSelect } from '../../../layout/context';
|
||||
import { ACTIONS, PANELS } from '../../../layout/enums';
|
||||
import { Layout } from '../../../layout/layoutTypes';
|
||||
import BreakoutDropdown from '../breakout-room-dropdown/component';
|
||||
import { BREAKOUT_ROOM_END_ALL, BREAKOUT_ROOM_REQUEST_JOIN_URL, USER_TRANSFER_VOICE_TO_MEETING } from '../../mutations';
|
||||
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
||||
import TimeRemaingPanel from './components/timeRemaining';
|
||||
import BreakoutMessageForm from './components/messageForm';
|
||||
import { CAMERA_BROADCAST_STOP } from '../../../video-provider/mutations';
|
||||
import {
|
||||
finishScreenShare,
|
||||
forceExitAudio,
|
||||
rejoinAudio,
|
||||
stopVideo,
|
||||
} from './service';
|
||||
|
||||
interface BreakoutRoomProps {
|
||||
breakouts: BreakoutRoom[];
|
||||
isModerator: boolean;
|
||||
presenter: boolean;
|
||||
durationInSeconds: number;
|
||||
userJoinedRooms: number;
|
||||
userJoinedAudio: boolean;
|
||||
userId: string;
|
||||
meetingId: string;
|
||||
}
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
breakoutTitle: {
|
||||
id: 'app.createBreakoutRoom.title',
|
||||
description: 'breakout title',
|
||||
},
|
||||
breakoutAriaTitle: {
|
||||
id: 'app.createBreakoutRoom.ariaTitle',
|
||||
description: 'breakout aria title',
|
||||
},
|
||||
breakoutDuration: {
|
||||
id: 'app.createBreakoutRoom.duration',
|
||||
description: 'breakout duration time',
|
||||
},
|
||||
breakoutRoom: {
|
||||
id: 'app.createBreakoutRoom.room',
|
||||
description: 'breakout room',
|
||||
},
|
||||
breakoutJoin: {
|
||||
id: 'app.createBreakoutRoom.join',
|
||||
description: 'label for join breakout room',
|
||||
},
|
||||
breakoutJoinAudio: {
|
||||
id: 'app.createBreakoutRoom.joinAudio',
|
||||
description: 'label for option to transfer audio',
|
||||
},
|
||||
breakoutReturnAudio: {
|
||||
id: 'app.createBreakoutRoom.returnAudio',
|
||||
description: 'label for option to return audio',
|
||||
},
|
||||
askToJoin: {
|
||||
id: 'app.createBreakoutRoom.askToJoin',
|
||||
description: 'label for generate breakout room url',
|
||||
},
|
||||
generatingURL: {
|
||||
id: 'app.createBreakoutRoom.generatingURL',
|
||||
description: 'label for generating breakout room url',
|
||||
},
|
||||
endAllBreakouts: {
|
||||
id: 'app.createBreakoutRoom.endAllBreakouts',
|
||||
description: 'Button label to end all breakout rooms',
|
||||
},
|
||||
chatTitleMsgAllRooms: {
|
||||
id: 'app.createBreakoutRoom.chatTitleMsgAllRooms',
|
||||
description: 'chat title for send message to all rooms',
|
||||
},
|
||||
alreadyConnected: {
|
||||
id: 'app.createBreakoutRoom.alreadyConnected',
|
||||
description: 'label for the user that is already connected to breakout room',
|
||||
},
|
||||
setTimeInMinutes: {
|
||||
id: 'app.createBreakoutRoom.setTimeInMinutes',
|
||||
description: 'Label for input to set time (minutes)',
|
||||
},
|
||||
setTimeLabel: {
|
||||
id: 'app.createBreakoutRoom.setTimeLabel',
|
||||
description: 'Button label to set breakout rooms time',
|
||||
},
|
||||
setTimeCancel: {
|
||||
id: 'app.createBreakoutRoom.setTimeCancel',
|
||||
description: 'Button label to cancel set breakout rooms time',
|
||||
},
|
||||
setTimeHigherThanMeetingTimeError: {
|
||||
id: 'app.createBreakoutRoom.setTimeHigherThanMeetingTimeError',
|
||||
description: 'Label for error when new breakout rooms time would be higher than remaining time in parent meeting',
|
||||
},
|
||||
});
|
||||
|
||||
const BreakoutRoom: React.FC<BreakoutRoomProps> = ({
|
||||
breakouts,
|
||||
isModerator,
|
||||
durationInSeconds,
|
||||
presenter,
|
||||
userJoinedRooms,
|
||||
userJoinedAudio,
|
||||
userId,
|
||||
meetingId,
|
||||
}) => {
|
||||
const [breakoutRoomEndAll] = useMutation(BREAKOUT_ROOM_END_ALL);
|
||||
const [breakoutRoomTransfer] = useMutation(USER_TRANSFER_VOICE_TO_MEETING);
|
||||
const [breakoutRoomRequestJoinURL] = useMutation(BREAKOUT_ROOM_REQUEST_JOIN_URL);
|
||||
const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP);
|
||||
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
const isRTL = layoutSelect((i: Layout) => i.isRTL);
|
||||
const intl = useIntl();
|
||||
|
||||
const panelRef = React.useRef<HTMLDivElement>(null);
|
||||
const [showChangeTimeForm, setShowChangeTimeForm] = React.useState(false);
|
||||
const [requestedBreakoutRoomId, setRequestedBreakoutRoomId] = React.useState<string>('');
|
||||
const [joinedRooms, setJoinedRooms] = React.useState<number>(0);
|
||||
|
||||
const sendUserUnshareWebcam = (cameraId: string) => {
|
||||
cameraBroadcastStop({ variables: { cameraId } });
|
||||
};
|
||||
|
||||
const transferUserToMeeting = (fromMeeting: string, toMeeting: string) => {
|
||||
breakoutRoomTransfer(
|
||||
{
|
||||
variables: {
|
||||
fromMeetingId: fromMeeting,
|
||||
toMeetingId: toMeeting,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const requestJoinURL = (breakoutRoomId: string) => {
|
||||
breakoutRoomRequestJoinURL({ variables: { breakoutRoomId } });
|
||||
};
|
||||
|
||||
const closePanel = useCallback(() => {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.NONE,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (userJoinedRooms !== joinedRooms) {
|
||||
setJoinedRooms((prev) => {
|
||||
if (userJoinedRooms === 0 && prev > 0) {
|
||||
rejoinAudio();
|
||||
}
|
||||
return userJoinedRooms;
|
||||
});
|
||||
}
|
||||
}, [userJoinedRooms]);
|
||||
|
||||
useEffect(() => {
|
||||
if (requestedBreakoutRoomId) {
|
||||
const breakout = breakouts.find((b) => b.breakoutRoomId === requestedBreakoutRoomId);
|
||||
if (breakout && breakout.joinURL) {
|
||||
window.open(breakout.joinURL, '_blank');
|
||||
setRequestedBreakoutRoomId('');
|
||||
}
|
||||
}
|
||||
}, [breakouts]);
|
||||
|
||||
return (
|
||||
<Styled.Panel
|
||||
ref={panelRef}
|
||||
onCopy={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Header
|
||||
leftButtonProps={{
|
||||
'aria-label': intl.formatMessage(intlMessages.breakoutAriaTitle),
|
||||
label: intl.formatMessage(intlMessages.breakoutTitle),
|
||||
onClick: closePanel,
|
||||
}}
|
||||
data-test="breakoutRoomManagerHeader"
|
||||
rightButtonProps={{}}
|
||||
customRightButton={isModerator && (
|
||||
<BreakoutDropdown
|
||||
openBreakoutTimeManager={() => setShowChangeTimeForm(true)}
|
||||
endAllBreakouts={() => {
|
||||
closePanel();
|
||||
breakoutRoomEndAll();
|
||||
}}
|
||||
isMeteorConnected
|
||||
amIModerator={isModerator}
|
||||
isRTL={isRTL}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<TimeRemaingPanel
|
||||
showChangeTimeForm={showChangeTimeForm}
|
||||
isModerator={isModerator}
|
||||
durationInSeconds={durationInSeconds}
|
||||
toggleShowChangeTimeForm={setShowChangeTimeForm}
|
||||
/>
|
||||
{isModerator ? <BreakoutMessageForm /> : null}
|
||||
{isModerator ? <Styled.Separator /> : null}
|
||||
<Styled.BreakoutsList>
|
||||
{
|
||||
breakouts.map((breakout) => {
|
||||
const breakoutLabel = breakout.joinURL
|
||||
? intl.formatMessage(intlMessages.breakoutJoin)
|
||||
: intl.formatMessage(intlMessages.askToJoin);
|
||||
const dataTest = `${breakout.joinURL ? 'join' : 'askToJoin'}${breakout.shortName.replace(' ', '')}`;
|
||||
const userJoinedDialin = breakout.participants.find((p) => p.userId === userId)?.isAudioOnly ?? false;
|
||||
return (
|
||||
<Styled.BreakoutItems key={`breakoutRoomItems-${breakout.breakoutRoomId}`}>
|
||||
<Styled.Content key={`breakoutRoomList-${breakout.breakoutRoomId}`}>
|
||||
<Styled.BreakoutRoomListNameLabel data-test={breakout.shortName} aria-hidden>
|
||||
{breakout.isDefaultName
|
||||
? intl.formatMessage(intlMessages.breakoutRoom, { 0: breakout.sequence })
|
||||
: breakout.shortName}
|
||||
<Styled.UsersAssignedNumberLabel>
|
||||
(
|
||||
{breakout.participants.length}
|
||||
)
|
||||
</Styled.UsersAssignedNumberLabel>
|
||||
</Styled.BreakoutRoomListNameLabel>
|
||||
{requestedBreakoutRoomId === breakout.breakoutRoomId ? (
|
||||
<span>
|
||||
{intl.formatMessage(intlMessages.generatingURL)}
|
||||
<Styled.ConnectingAnimation animations />
|
||||
</span>
|
||||
) : (
|
||||
<Styled.BreakoutActions>
|
||||
{
|
||||
breakout.currentRoomJoined
|
||||
? (
|
||||
<Styled.AlreadyConnected data-test="alreadyConnected">
|
||||
{intl.formatMessage(intlMessages.alreadyConnected)}
|
||||
</Styled.AlreadyConnected>
|
||||
)
|
||||
: (
|
||||
<Styled.JoinButton
|
||||
label={breakoutLabel}
|
||||
data-test={dataTest}
|
||||
aria-label={`${breakoutLabel} ${breakout.shortName}`}
|
||||
onClick={() => {
|
||||
if (!breakout.joinURL) {
|
||||
setRequestedBreakoutRoomId(breakout.breakoutRoomId);
|
||||
requestJoinURL(breakout.breakoutRoomId);
|
||||
} else {
|
||||
window.open(breakout.joinURL, '_blank');
|
||||
// leave main room's audio,
|
||||
// and stops video and screenshare when joining a breakout room
|
||||
forceExitAudio();
|
||||
stopVideo(sendUserUnshareWebcam);
|
||||
logger.info({
|
||||
logCode: 'breakoutroom_join',
|
||||
extraInfo: { logType: 'user_action' },
|
||||
}, 'joining breakout room closed audio in the main room');
|
||||
if (presenter) finishScreenShare();
|
||||
}
|
||||
}}
|
||||
disabled={requestedBreakoutRoomId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
isModerator && (userJoinedAudio || userJoinedDialin)
|
||||
? [
|
||||
('|'),
|
||||
(
|
||||
<Styled.AudioButton
|
||||
label={
|
||||
userJoinedDialin
|
||||
? intl.formatMessage(intlMessages.breakoutReturnAudio)
|
||||
: intl.formatMessage(intlMessages.breakoutJoinAudio)
|
||||
}
|
||||
disabled={false}
|
||||
key={`join-audio-${breakout.breakoutRoomId}`}
|
||||
onClick={
|
||||
userJoinedDialin ? () => transferUserToMeeting(breakout.breakoutRoomId, meetingId)
|
||||
: () => transferUserToMeeting(meetingId, breakout.breakoutRoomId)
|
||||
}
|
||||
/>
|
||||
),
|
||||
]
|
||||
: null
|
||||
}
|
||||
</Styled.BreakoutActions>
|
||||
)}
|
||||
</Styled.Content>
|
||||
<Styled.JoinedUserNames
|
||||
data-test={`userNameBreakoutRoom-${breakout.shortName}`}
|
||||
>
|
||||
{breakout.participants
|
||||
.filter((p) => !p.isAudioOnly)
|
||||
.sort((a, b) => a.user.nameSortable.localeCompare(b.user.nameSortable))
|
||||
.map((u) => u.user.name)
|
||||
.join(', ')}
|
||||
</Styled.JoinedUserNames>
|
||||
</Styled.BreakoutItems>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Styled.BreakoutsList>
|
||||
</Styled.Panel>
|
||||
);
|
||||
};
|
||||
|
||||
const BreakoutRoomContainer: React.FC = () => {
|
||||
const {
|
||||
data: meetingData,
|
||||
} = useMeeting((m) => ({
|
||||
durationInSeconds: m.durationInSeconds,
|
||||
meetingId: m.meetingId,
|
||||
}));
|
||||
|
||||
const {
|
||||
data: currentUserData,
|
||||
loading: currentUserLoading,
|
||||
} = useCurrentUser((u) => ({
|
||||
isModerator: u.isModerator,
|
||||
presenter: u.presenter,
|
||||
voice: u.voice,
|
||||
userId: u.userId,
|
||||
}));
|
||||
|
||||
const {
|
||||
data: getIfUserJoinedBreakoutRoomData,
|
||||
loading: getIfUserJoinedBreakoutRoomLoading,
|
||||
} = useSubscription<GetIfUserJoinedBreakoutRoomResponse>(getIfUserJoinedBreakoutRoom);
|
||||
|
||||
const {
|
||||
data: breakoutData,
|
||||
loading: breakoutLoading,
|
||||
error: breakoutError,
|
||||
} = useSubscription<GetBreakoutDataResponse>(getBreakoutData);
|
||||
|
||||
if (
|
||||
breakoutLoading
|
||||
|| currentUserLoading
|
||||
|| getIfUserJoinedBreakoutRoomLoading
|
||||
) return null;
|
||||
|
||||
if (breakoutError) {
|
||||
logger.error(breakoutError);
|
||||
return (
|
||||
<div>
|
||||
Error:
|
||||
{JSON.stringify(breakoutError)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!currentUserData || !breakoutData || !meetingData) return null; // or loading spinner or error
|
||||
return (
|
||||
<BreakoutRoom
|
||||
breakouts={breakoutData.breakoutRoom || []}
|
||||
isModerator={currentUserData.isModerator ?? false}
|
||||
presenter={currentUserData.presenter ?? false}
|
||||
durationInSeconds={meetingData.durationInSeconds ?? 0}
|
||||
userJoinedRooms={getIfUserJoinedBreakoutRoomData?.breakoutRoom_aggregate.aggregate.count ?? 0}
|
||||
userJoinedAudio={currentUserData?.voice?.joined ?? false}
|
||||
userId={currentUserData.userId ?? ''}
|
||||
meetingId={meetingData.meetingId ?? ''}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default BreakoutRoomContainer;
|
@ -0,0 +1,158 @@
|
||||
import { useMutation } from '@apollo/client';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { escapeHtml } from '/imports/utils/string-utils';
|
||||
import { BREAKOUT_ROOM_SEND_MESSAGE_TO_ALL } from '../../../mutations';
|
||||
import Styled from '../styles';
|
||||
|
||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||
const minMessageLength = CHAT_CONFIG.min_message_length;
|
||||
const maxMessageLength = CHAT_CONFIG.max_message_length;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
submitLabel: {
|
||||
id: 'app.chat.submitLabel',
|
||||
description: 'Chat submit button label',
|
||||
},
|
||||
inputLabel: {
|
||||
id: 'app.chat.inputLabel',
|
||||
description: 'Chat message input label',
|
||||
},
|
||||
inputPlaceholder: {
|
||||
id: 'app.chat.inputPlaceholder',
|
||||
description: 'Chat message input placeholder',
|
||||
},
|
||||
errorMaxMessageLength: {
|
||||
id: 'app.chat.errorMaxMessageLength',
|
||||
},
|
||||
errorMinMessageLength: {
|
||||
id: 'app.chat.errorMinMessageLength',
|
||||
},
|
||||
errorServerDisconnected: {
|
||||
id: 'app.chat.disconnected',
|
||||
},
|
||||
errorChatLocked: {
|
||||
id: 'app.chat.locked',
|
||||
},
|
||||
chatTitleMsgAllRooms: {
|
||||
id: 'app.createBreakoutRoom.chatTitleMsgAllRooms',
|
||||
description: 'chat title for send message to all rooms',
|
||||
},
|
||||
});
|
||||
|
||||
const BreakoutMessageForm: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [message, setMessage] = React.useState('');
|
||||
const [error, setError] = React.useState('');
|
||||
const [hasErrors, setHasErrors] = React.useState(false);
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>();
|
||||
const chatTitle = useRef(intl.formatMessage(intlMessages.chatTitleMsgAllRooms));
|
||||
|
||||
const [sendMessageToAllBreakouts] = useMutation(BREAKOUT_ROOM_SEND_MESSAGE_TO_ALL);
|
||||
|
||||
useEffect(() => {
|
||||
const unSentMessage = sessionStorage.getItem('breakoutUnsentMessage');
|
||||
if (unSentMessage) {
|
||||
setMessage(unSentMessage);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const verifyForErrors = useCallback((message: string) => {
|
||||
if (message.length < minMessageLength) {
|
||||
if (!hasErrors) setHasErrors(true);
|
||||
setError(intl.formatMessage(intlMessages.errorMaxMessageLength, { minMessageLength }));
|
||||
} else if ((message.length > maxMessageLength) && !hasErrors) {
|
||||
if (!hasErrors) setHasErrors(true);
|
||||
setError(intl.formatMessage(intlMessages.errorMaxMessageLength, { maxMessageLength }));
|
||||
return true;
|
||||
} else {
|
||||
setHasErrors(false);
|
||||
setError('');
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
const editMessage = useCallback((message: string) => {
|
||||
verifyForErrors(message);
|
||||
setMessage(message);
|
||||
sessionStorage.setItem('breakoutUnsentMessage', message);
|
||||
textAreaRef?.current?.focus();
|
||||
}, []);
|
||||
|
||||
const sendMessage = useCallback((message: string) => {
|
||||
sendMessageToAllBreakouts({
|
||||
variables: {
|
||||
message,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSendMessage = useCallback((message: string) => {
|
||||
if (!verifyForErrors(message)) {
|
||||
sendMessage(escapeHtml(message));
|
||||
setMessage('');
|
||||
setError('');
|
||||
setHasErrors(false);
|
||||
sessionStorage.removeItem('breakoutUnsentMessage');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage(message);
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
const handleSubmit = useCallback((e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
handleSendMessage(message);
|
||||
}, [message]);
|
||||
|
||||
const handleOnChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
editMessage(e.target.value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Styled.Form
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Styled.Wrapper>
|
||||
<Styled.Input
|
||||
id="message-input"
|
||||
innerRef={(ref) => { textAreaRef.current = ref; }}
|
||||
placeholder={intl.formatMessage(intlMessages.inputPlaceholder, { 0: chatTitle.current })}
|
||||
aria-label={intl.formatMessage(intlMessages.inputLabel, { 0: chatTitle.current })}
|
||||
aria-invalid={hasErrors ? 'true' : 'false'}
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
spellCheck="true"
|
||||
value={message}
|
||||
onChange={handleOnChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
async
|
||||
onPaste={(e) => { e.stopPropagation(); }}
|
||||
onCut={(e) => { e.stopPropagation(); }}
|
||||
onCopy={(e) => { e.stopPropagation(); }}
|
||||
/>
|
||||
<Styled.SendButton
|
||||
hideLabel
|
||||
circle
|
||||
aria-label={intl.formatMessage(intlMessages.submitLabel)}
|
||||
type="submit"
|
||||
disabled={hasErrors || !message}
|
||||
label={intl.formatMessage(intlMessages.submitLabel)}
|
||||
color="primary"
|
||||
icon="send"
|
||||
onClick={() => {}}
|
||||
data-test="sendMessageButton"
|
||||
/>
|
||||
</Styled.Wrapper>
|
||||
{ hasErrors ? <Styled.ErrorMessage>{error}</Styled.ErrorMessage> : null }
|
||||
</Styled.Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default BreakoutMessageForm;
|
@ -0,0 +1,157 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import BreakoutRemainingTime from '/imports/ui/components/common/remaining-time/breakout-duration/component';
|
||||
import Styled from '../styles';
|
||||
import { BREAKOUT_ROOM_SET_TIME } from '../../../mutations';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
breakoutTitle: {
|
||||
id: 'app.createBreakoutRoom.title',
|
||||
description: 'breakout title',
|
||||
},
|
||||
breakoutAriaTitle: {
|
||||
id: 'app.createBreakoutRoom.ariaTitle',
|
||||
description: 'breakout aria title',
|
||||
},
|
||||
breakoutDuration: {
|
||||
id: 'app.createBreakoutRoom.duration',
|
||||
description: 'breakout duration time',
|
||||
},
|
||||
breakoutRoom: {
|
||||
id: 'app.createBreakoutRoom.room',
|
||||
description: 'breakout room',
|
||||
},
|
||||
breakoutJoin: {
|
||||
id: 'app.createBreakoutRoom.join',
|
||||
description: 'label for join breakout room',
|
||||
},
|
||||
breakoutJoinAudio: {
|
||||
id: 'app.createBreakoutRoom.joinAudio',
|
||||
description: 'label for option to transfer audio',
|
||||
},
|
||||
breakoutReturnAudio: {
|
||||
id: 'app.createBreakoutRoom.returnAudio',
|
||||
description: 'label for option to return audio',
|
||||
},
|
||||
askToJoin: {
|
||||
id: 'app.createBreakoutRoom.askToJoin',
|
||||
description: 'label for generate breakout room url',
|
||||
},
|
||||
generatingURL: {
|
||||
id: 'app.createBreakoutRoom.generatingURL',
|
||||
description: 'label for generating breakout room url',
|
||||
},
|
||||
endAllBreakouts: {
|
||||
id: 'app.createBreakoutRoom.endAllBreakouts',
|
||||
description: 'Button label to end all breakout rooms',
|
||||
},
|
||||
chatTitleMsgAllRooms: {
|
||||
id: 'app.createBreakoutRoom.chatTitleMsgAllRooms',
|
||||
description: 'chat title for send message to all rooms',
|
||||
},
|
||||
alreadyConnected: {
|
||||
id: 'app.createBreakoutRoom.alreadyConnected',
|
||||
description: 'label for the user that is already connected to breakout room',
|
||||
},
|
||||
setTimeInMinutes: {
|
||||
id: 'app.createBreakoutRoom.setTimeInMinutes',
|
||||
description: 'Label for input to set time (minutes)',
|
||||
},
|
||||
setTimeLabel: {
|
||||
id: 'app.createBreakoutRoom.setTimeLabel',
|
||||
description: 'Button label to set breakout rooms time',
|
||||
},
|
||||
setTimeCancel: {
|
||||
id: 'app.createBreakoutRoom.setTimeCancel',
|
||||
description: 'Button label to cancel set breakout rooms time',
|
||||
},
|
||||
setTimeHigherThanMeetingTimeError: {
|
||||
id: 'app.createBreakoutRoom.setTimeHigherThanMeetingTimeError',
|
||||
description: 'Label for error when new breakout rooms time would be higher than remaining time in parent meeting',
|
||||
},
|
||||
});
|
||||
|
||||
interface TimeRemainingPanelProps {
|
||||
showChangeTimeForm: boolean;
|
||||
isModerator: boolean;
|
||||
durationInSeconds: number;
|
||||
toggleShowChangeTimeForm: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const TimeRemaingPanel: React.FC<TimeRemainingPanelProps> = ({
|
||||
showChangeTimeForm,
|
||||
isModerator,
|
||||
durationInSeconds,
|
||||
toggleShowChangeTimeForm,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const durationContainerRef = React.useRef(null);
|
||||
const [showFormError, setShowFormError] = useState(false);
|
||||
const [newTime, setNewTime] = useState(0);
|
||||
|
||||
const [breakoutRoomSetTime] = useMutation(BREAKOUT_ROOM_SET_TIME);
|
||||
|
||||
const setBreakoutsTime = (timeInMinutes: number) => {
|
||||
if (timeInMinutes <= 0) return false;
|
||||
|
||||
return breakoutRoomSetTime({ variables: { timeInMinutes } });
|
||||
};
|
||||
|
||||
return (
|
||||
<Styled.DurationContainer
|
||||
centeredText={!showChangeTimeForm}
|
||||
ref={durationContainerRef}
|
||||
>
|
||||
<BreakoutRemainingTime
|
||||
boldText
|
||||
/>
|
||||
{isModerator && showChangeTimeForm ? (
|
||||
<Styled.SetTimeContainer>
|
||||
<label htmlFor="inputSetTimeSelector">
|
||||
{intl.formatMessage(intlMessages.setTimeInMinutes)}
|
||||
</label>
|
||||
<br />
|
||||
<Styled.FlexRow>
|
||||
<Styled.SetDurationInput
|
||||
id="inputSetTimeSelector"
|
||||
type="number"
|
||||
min="1"
|
||||
value={newTime}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newSetTime = Number.parseInt(e.target.value, 10) || 0;
|
||||
setNewTime(newSetTime);
|
||||
}}
|
||||
aria-label={intl.formatMessage(intlMessages.setTimeInMinutes)}
|
||||
/>
|
||||
|
||||
|
||||
<Styled.EndButton
|
||||
data-test="sendButtonDurationTime"
|
||||
color="primary"
|
||||
disabled={false}
|
||||
size="sm"
|
||||
label={intl.formatMessage(intlMessages.setTimeLabel)}
|
||||
onClick={() => {
|
||||
setShowFormError(false);
|
||||
|
||||
if (durationInSeconds !== 0 && newTime > durationInSeconds) {
|
||||
setShowFormError(true);
|
||||
} else if (setBreakoutsTime(newTime)) {
|
||||
toggleShowChangeTimeForm(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Styled.FlexRow>
|
||||
{showFormError ? (
|
||||
<Styled.WithError>
|
||||
{intl.formatMessage(intlMessages.setTimeHigherThanMeetingTimeError)}
|
||||
</Styled.WithError>
|
||||
) : null}
|
||||
</Styled.SetTimeContainer>
|
||||
) : null}
|
||||
</Styled.DurationContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimeRemaingPanel;
|
@ -0,0 +1,72 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export interface BreakoutRoom {
|
||||
freeJoin: boolean;
|
||||
shortName: string;
|
||||
sendInvitationToModerators: boolean;
|
||||
sequence: number;
|
||||
showInvitation: boolean;
|
||||
joinURL: string | null;
|
||||
breakoutRoomId: string;
|
||||
isDefaultName: boolean;
|
||||
currentRoomJoined: boolean;
|
||||
participants: Array<{
|
||||
userId: string;
|
||||
isAudioOnly: string;
|
||||
user: {
|
||||
name: string;
|
||||
nameSortable: string;
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export interface GetBreakoutDataResponse {
|
||||
breakoutRoom: BreakoutRoom[];
|
||||
}
|
||||
|
||||
export interface GetIfUserJoinedBreakoutRoomResponse {
|
||||
breakoutRoom_aggregate:{
|
||||
aggregate: {
|
||||
count: number;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getBreakoutData = gql`
|
||||
subscription getBreakoutData {
|
||||
breakoutRoom(order_by: {sequence: asc}){
|
||||
freeJoin
|
||||
shortName
|
||||
sendInvitationToModerators
|
||||
sequence
|
||||
showInvitation
|
||||
joinURL
|
||||
breakoutRoomId
|
||||
isDefaultName
|
||||
currentRoomJoined
|
||||
participants {
|
||||
userId
|
||||
isAudioOnly
|
||||
user {
|
||||
name
|
||||
nameSortable
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const getIfUserJoinedBreakoutRoom = gql`
|
||||
subscription getIdUserJoinedABreakout {
|
||||
breakoutRoom_aggregate(where: {currentRoomJoined: {_eq: true}}) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
getBreakoutData,
|
||||
getIfUserJoinedBreakoutRoom,
|
||||
};
|
@ -0,0 +1,67 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import AudioService from '/imports/ui/components/audio/service';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import AudioManager from '/imports/ui/services/audio-manager';
|
||||
import VideoService from '/imports/ui/components/video-provider/service';
|
||||
import { screenshareHasEnded } from '/imports/ui/components/screenshare/service';
|
||||
import { didUserSelectedListenOnly, didUserSelectedMicrophone } from '../../../audio/audio-modal/service';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
|
||||
export const getIsMicrophoneUser = () => {
|
||||
return (AudioService.isConnectedToBreakout() || AudioService.isConnected())
|
||||
&& !AudioService.isListenOnly();
|
||||
};
|
||||
|
||||
export const getIsReconnecting = () => {
|
||||
return AudioService.isReconnecting();
|
||||
};
|
||||
|
||||
export const getIsConnected = () => {
|
||||
return Meteor.status().connected;
|
||||
};
|
||||
|
||||
export const endAllBreakouts = () => {
|
||||
makeCall('endAllBreakouts');
|
||||
};
|
||||
|
||||
export const forceExitAudio = () => {
|
||||
AudioManager.forceExitAudio();
|
||||
};
|
||||
|
||||
export const stopVideo = (unshareVideo: (stream: string)=> void) => {
|
||||
VideoService.storeDeviceIds();
|
||||
VideoService.exitVideo(unshareVideo);
|
||||
};
|
||||
|
||||
export const finishScreenShare = () => {
|
||||
return screenshareHasEnded();
|
||||
};
|
||||
|
||||
const logUserCouldNotRejoinAudio = () => {
|
||||
logger.warn({
|
||||
logCode: 'mainroom_audio_rejoin',
|
||||
extraInfo: { logType: 'user_action' },
|
||||
}, 'leaving breakout room couldn\'t rejoin audio in the main room');
|
||||
};
|
||||
|
||||
export const rejoinAudio = () => {
|
||||
if (didUserSelectedMicrophone()) {
|
||||
AudioManager.joinMicrophone().catch(() => {
|
||||
logUserCouldNotRejoinAudio();
|
||||
});
|
||||
} else if (didUserSelectedListenOnly()) {
|
||||
AudioManager.joinListenOnly().catch(() => {
|
||||
logUserCouldNotRejoinAudio();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
getIsMicrophoneUser,
|
||||
getIsReconnecting,
|
||||
endAllBreakouts,
|
||||
forceExitAudio,
|
||||
stopVideo,
|
||||
finishScreenShare,
|
||||
rejoinAudio,
|
||||
};
|
@ -0,0 +1,373 @@
|
||||
import styled, { css, keyframes } from 'styled-components';
|
||||
import {
|
||||
mdPaddingX,
|
||||
borderSize,
|
||||
borderSizeSmall,
|
||||
borderRadius,
|
||||
jumboPaddingY,
|
||||
smPaddingX,
|
||||
smPaddingY,
|
||||
} from '/imports/ui/stylesheets/styled-components/general';
|
||||
import {
|
||||
colorPrimary,
|
||||
colorGray,
|
||||
colorDanger,
|
||||
userListBg,
|
||||
colorWhite,
|
||||
colorGrayLighter,
|
||||
colorGrayLightest,
|
||||
colorBlueLight,
|
||||
listItemBgHover,
|
||||
colorText,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import {
|
||||
headingsFontWeight,
|
||||
fontSizeSmall,
|
||||
fontSizeBase,
|
||||
} from '/imports/ui/stylesheets/styled-components/typography';
|
||||
import { ScrollboxVertical } from '/imports/ui/stylesheets/styled-components/scrollable';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
|
||||
const BreakoutActions = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
font-weight: ${headingsFontWeight};
|
||||
color: ${colorPrimary};
|
||||
|
||||
& > button {
|
||||
padding: 0 0 0 .5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const AlreadyConnected = styled.span`
|
||||
padding: 0 .5rem 0 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
// @ts-ignore - as button comes from JS, we can't provide its props
|
||||
const JoinButton = styled(Button)`
|
||||
flex: 0 1 48%;
|
||||
color: ${colorPrimary};
|
||||
margin: 0;
|
||||
font-weight: inherit;
|
||||
padding: 0 .5rem 0 .5rem !important;
|
||||
`;
|
||||
// @ts-ignore - as button comes from JS, we can't provide its props
|
||||
const AudioButton = styled(Button)`
|
||||
flex: 0 1 48%;
|
||||
color: ${colorPrimary};
|
||||
margin: 0;
|
||||
font-weight: inherit;
|
||||
`;
|
||||
|
||||
const BreakoutItems = styled.div`
|
||||
margin-bottom: 1rem;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
font-size: ${fontSizeSmall};
|
||||
font-weight: bold;
|
||||
padding: ${borderSize} ${borderSize} ${borderSize} 0;
|
||||
|
||||
[dir="rtl"] & {
|
||||
padding: ${borderSize} 0 ${borderSize} ${borderSize};
|
||||
}
|
||||
`;
|
||||
|
||||
const BreakoutRoomListNameLabel = styled.span`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const UsersAssignedNumberLabel = styled.span`
|
||||
margin: 0 0 0 .25rem;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: 0 .25em 0 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const ellipsis = keyframes`
|
||||
to {
|
||||
width: 1.5em;
|
||||
}
|
||||
`;
|
||||
|
||||
type ConnectingAnimationProps = {
|
||||
animations: boolean;
|
||||
|
||||
};
|
||||
const ConnectingAnimation = styled.span<ConnectingAnimationProps>`
|
||||
&:after {
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
content: "\\2026"; /* ascii code for the ellipsis character */
|
||||
width: 0;
|
||||
margin: 0 1.25em 0 0;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: 0 0 0 1.25em;
|
||||
}
|
||||
|
||||
${({ animations }) => animations && css`
|
||||
animation: ${ellipsis} steps(4, end) 900ms infinite;
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
const BreakoutsList = styled.div`
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const JoinedUserNames = styled.div`
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-line;
|
||||
margin-left: 1rem;
|
||||
font-size: ${fontSizeSmall};
|
||||
`;
|
||||
|
||||
const BreakoutColumn = styled.div`
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
min-height: 0;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
const BreakoutScrollableList = styled(ScrollboxVertical)`
|
||||
background: linear-gradient(${userListBg} 30%, rgba(255,255,255,0)),
|
||||
linear-gradient(rgba(255,255,255,0), ${userListBg} 70%) 0 100%,
|
||||
radial-gradient(farthest-side at 50% 0, rgba(0,0,0,.2), rgba(0,0,0,0)),
|
||||
radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.2), rgba(0,0,0,0)) 0 100%;
|
||||
|
||||
outline: transparent;
|
||||
outline-style: dotted;
|
||||
outline-width: ${borderSize};
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-radius: ${borderSize};
|
||||
box-shadow: 0 0 0 ${borderSize} ${listItemBgHover}, inset 0 0 0 1px ${colorPrimary};
|
||||
}
|
||||
|
||||
&:focus-within,
|
||||
&:focus {
|
||||
outline-style: solid;
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: none;
|
||||
border-radius: none;
|
||||
}
|
||||
|
||||
overflow-x: hidden;
|
||||
outline-width: 1px !important;
|
||||
outline-color: transparent !important;
|
||||
background: none;
|
||||
`;
|
||||
|
||||
type DurationContainerProps = {
|
||||
centeredText: boolean;
|
||||
};
|
||||
|
||||
const DurationContainer = styled.div<DurationContainerProps>`
|
||||
${({ centeredText }) => centeredText && `
|
||||
text-align: center;
|
||||
`}
|
||||
|
||||
border-radius: ${borderRadius};
|
||||
margin-bottom: ${jumboPaddingY};
|
||||
padding: 10px;
|
||||
box-shadow: 0 0 1px 1px ${colorGrayLightest};
|
||||
`;
|
||||
|
||||
const SetTimeContainer = styled.div`
|
||||
margin: .5rem 0 0 0;
|
||||
`;
|
||||
|
||||
const SetDurationInput = styled.input`
|
||||
flex: 1;
|
||||
border: 1px solid ${colorGrayLighter};
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
padding: .25rem;
|
||||
border-radius: ${borderRadius};
|
||||
background-clip: padding-box;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: ${colorGray};
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-radius: ${borderSize};
|
||||
box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary};
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
opacity: .75;
|
||||
background-color: rgba(167,179,189,0.25);
|
||||
}
|
||||
`;
|
||||
|
||||
const WithError = styled.span`
|
||||
color: ${colorDanger};
|
||||
`;
|
||||
// @ts-ignore - as button comes from JS, we can't provide its props
|
||||
const EndButton = styled(Button)`
|
||||
padding: .5rem;
|
||||
font-weight: ${headingsFontWeight} !important;
|
||||
border-radius: .2rem;
|
||||
font-size: ${fontSizeSmall};
|
||||
`;
|
||||
|
||||
const Duration = styled.span`
|
||||
display: inline-block;
|
||||
align-self: center;
|
||||
`;
|
||||
|
||||
const Panel = styled(ScrollboxVertical)`
|
||||
background: linear-gradient(${colorWhite} 30%, rgba(255,255,255,0)),
|
||||
linear-gradient(rgba(255,255,255,0), ${colorWhite} 70%) 0 100%,
|
||||
radial-gradient(farthest-side at 50% 0, rgba(0,0,0,.2), rgba(0,0,0,0)),
|
||||
radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.2), rgba(0,0,0,0)) 0 100%;
|
||||
|
||||
background-color: #fff;
|
||||
padding: ${mdPaddingX};
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const Separator = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
height: ${borderSizeSmall};
|
||||
background-color: ${colorGrayLighter};
|
||||
margin: 30px 0px;
|
||||
`;
|
||||
|
||||
const FlexRow = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
`;
|
||||
|
||||
const Form = styled.form`
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
align-self: flex-end;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin-bottom: calc(-1 * ${smPaddingX});
|
||||
margin-top: .2rem;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const Input = styled(TextareaAutosize)`
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
background-clip: padding-box;
|
||||
margin: 0;
|
||||
color: ${colorText};
|
||||
-webkit-appearance: none;
|
||||
padding: calc(${smPaddingY} * 2.5) calc(${smPaddingX} * 1.25);
|
||||
resize: none;
|
||||
transition: none;
|
||||
border-radius: ${borderRadius};
|
||||
font-size: ${fontSizeBase};
|
||||
line-height: 1;
|
||||
min-height: 2.5rem;
|
||||
max-height: 10rem;
|
||||
border: 1px solid ${colorGrayLighter};
|
||||
|
||||
&:disabled,
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
opacity: .75;
|
||||
background-color: rgba(167,179,189,0.25);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-radius: ${borderSize};
|
||||
box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary};
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: transparent;
|
||||
outline-style: dotted;
|
||||
outline-width: ${borderSize};
|
||||
}
|
||||
`;
|
||||
// @ts-ignore - as button comes from JS, we can't provide its props
|
||||
const SendButton = styled(Button)`
|
||||
margin:0 0 0 ${smPaddingX};
|
||||
align-self: center;
|
||||
font-size: 0.9rem;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: 0 ${smPaddingX} 0 0;
|
||||
-webkit-transform: scale(-1, 1);
|
||||
-moz-transform: scale(-1, 1);
|
||||
-ms-transform: scale(-1, 1);
|
||||
-o-transform: scale(-1, 1);
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
`;
|
||||
|
||||
const ErrorMessage = styled.div`
|
||||
color: ${colorDanger};
|
||||
font-size: calc(${fontSizeBase} * .75);
|
||||
text-align: left;
|
||||
padding: ${borderSize} 0;
|
||||
position: relative;
|
||||
height: .93rem;
|
||||
max-height: .93rem;
|
||||
`;
|
||||
|
||||
export default {
|
||||
BreakoutActions,
|
||||
AlreadyConnected,
|
||||
JoinButton,
|
||||
AudioButton,
|
||||
BreakoutItems,
|
||||
Content,
|
||||
BreakoutRoomListNameLabel,
|
||||
UsersAssignedNumberLabel,
|
||||
ConnectingAnimation,
|
||||
JoinedUserNames,
|
||||
BreakoutColumn,
|
||||
BreakoutScrollableList,
|
||||
DurationContainer,
|
||||
SetTimeContainer,
|
||||
SetDurationInput,
|
||||
WithError,
|
||||
EndButton,
|
||||
Duration,
|
||||
Panel,
|
||||
Separator,
|
||||
FlexRow,
|
||||
Form,
|
||||
Wrapper,
|
||||
Input,
|
||||
SendButton,
|
||||
ErrorMessage,
|
||||
BreakoutsList,
|
||||
};
|
@ -35,7 +35,7 @@ const DEFAULT_BREAKOUT_TIME = 15;
|
||||
interface CreateBreakoutRoomContainerProps {
|
||||
isOpen: boolean
|
||||
setIsOpen: (isOpen: boolean) => void
|
||||
priority: number,
|
||||
priority: string,
|
||||
isUpdate?: boolean,
|
||||
}
|
||||
|
||||
|
@ -174,13 +174,21 @@ const RoomManagmentState: React.FC<RoomManagmentStateProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (runningRooms && init) {
|
||||
const usersToMove: string[] = [];
|
||||
const toRooms: number[] = [];
|
||||
|
||||
runningRooms.forEach((r: breakoutRoom) => {
|
||||
r.participants.forEach((u) => {
|
||||
if (!rooms[r.sequence]?.users?.find((user) => user.userId === u.user.userId)) {
|
||||
moveUser(u.user.userId, 0, r.sequence);
|
||||
usersToMove.push(u.user.userId);
|
||||
toRooms.push(r.sequence);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (usersToMove.length > 0) {
|
||||
moveUser(usersToMove, 0, toRooms);
|
||||
}
|
||||
}
|
||||
}, [runningRooms, init]);
|
||||
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import { CAMERA_BROADCAST_STOP } from '../video-provider/mutations';
|
||||
import useToggleVoice from '../audio/audio-graphql/hooks/useToggleVoice';
|
||||
import BreakoutContainerGraphql from '/imports/ui/components/breakout-room/breakout-room-graphql/breakout-room/component';
|
||||
|
||||
const BreakoutContainer = (props) => {
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
@ -92,15 +93,7 @@ const BreakoutContainer = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
return <BreakoutComponent
|
||||
amIPresenter={amIPresenter}
|
||||
endAllBreakouts={endAllBreakouts}
|
||||
setBreakoutsTime={setBreakoutsTime}
|
||||
transferUserToMeeting={transferUserToMeeting}
|
||||
requestJoinURL={requestJoinURL}
|
||||
sendUserUnshareWebcam={sendUserUnshareWebcam}
|
||||
{...{ layoutContextDispatch, isRTL, amIModerator, rejoinAudio, ...props }}
|
||||
/>;
|
||||
return <BreakoutContainerGraphql />;
|
||||
};
|
||||
|
||||
export default withTracker((props) => {
|
||||
|
@ -1,41 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Styled from './styles';
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
handleOnClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
start: {
|
||||
id: 'app.actionsBar.captions.start',
|
||||
description: 'Start closed captions option',
|
||||
},
|
||||
stop: {
|
||||
id: 'app.actionsBar.captions.stop',
|
||||
description: 'Stop closed captions option',
|
||||
},
|
||||
});
|
||||
|
||||
const CaptionsButton = ({ intl, isActive, handleOnClick }) => (
|
||||
<Styled.CaptionsButton
|
||||
icon="closed_caption"
|
||||
label={intl.formatMessage(isActive ? intlMessages.stop : intlMessages.start)}
|
||||
color={isActive ? 'primary' : 'default'}
|
||||
ghost={!isActive}
|
||||
hideLabel
|
||||
circle
|
||||
size="lg"
|
||||
onClick={handleOnClick}
|
||||
id={isActive ? 'stop-captions-button' : 'start-captions-button'}
|
||||
data-test="startViewingClosedCaptionsBtn"
|
||||
/>
|
||||
);
|
||||
|
||||
CaptionsButton.propTypes = propTypes;
|
||||
export default injectIntl(CaptionsButton);
|
@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Service from '/imports/ui/components/captions/service';
|
||||
import CaptionButton from './component';
|
||||
|
||||
const Container = (props) => <CaptionButton {...props} />;
|
||||
|
||||
export default withTracker(({ setIsOpen }) => ({
|
||||
isActive: Service.isCaptionsActive(),
|
||||
handleOnClick: () => (Service.isCaptionsActive()
|
||||
? Service.deactivateCaptions()
|
||||
: setIsOpen(true)),
|
||||
}))(Container);
|
@ -1,17 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
|
||||
const CaptionsButton = styled(Button)`
|
||||
${({ ghost }) => ghost && `
|
||||
span {
|
||||
box-shadow: none;
|
||||
background-color: transparent !important;
|
||||
border-color: ${colorWhite} !important;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export default {
|
||||
CaptionsButton,
|
||||
};
|
@ -1,131 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import PadContainer from '/imports/ui/components/pads/container';
|
||||
import Service from '/imports/ui/components/captions/service';
|
||||
import Styled from './styles';
|
||||
import { PANELS, ACTIONS } from '/imports/ui/components/layout/enums';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import Header from '/imports/ui/components/common/control-header/component';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
hide: {
|
||||
id: 'app.captions.hide',
|
||||
description: 'Label for hiding closed captions',
|
||||
},
|
||||
takeOwnership: {
|
||||
id: 'app.captions.ownership',
|
||||
description: 'Label for taking ownership of closed captions',
|
||||
},
|
||||
takeOwnershipTooltip: {
|
||||
id: 'app.captions.ownershipTooltip',
|
||||
description: 'Text for button for taking ownership of closed captions',
|
||||
},
|
||||
dictationStart: {
|
||||
id: 'app.captions.dictationStart',
|
||||
description: 'Label for starting speech recognition',
|
||||
},
|
||||
dictationStop: {
|
||||
id: 'app.captions.dictationStop',
|
||||
description: 'Label for stopping speech recognition',
|
||||
},
|
||||
dictationOnDesc: {
|
||||
id: 'app.captions.dictationOnDesc',
|
||||
description: 'Aria description for button that turns on speech recognition',
|
||||
},
|
||||
dictationOffDesc: {
|
||||
id: 'app.captions.dictationOffDesc',
|
||||
description: 'Aria description for button that turns off speech recognition',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
locale: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
ownerId: PropTypes.string.isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
dictation: PropTypes.bool.isRequired,
|
||||
dictating: PropTypes.bool.isRequired,
|
||||
isRTL: PropTypes.bool.isRequired,
|
||||
hasPermission: PropTypes.bool.isRequired,
|
||||
layoutContextDispatch: PropTypes.func.isRequired,
|
||||
isResizing: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const Captions = ({
|
||||
locale,
|
||||
intl,
|
||||
ownerId,
|
||||
name,
|
||||
dictation,
|
||||
dictating,
|
||||
isRTL,
|
||||
hasPermission,
|
||||
layoutContextDispatch,
|
||||
isResizing,
|
||||
}) => {
|
||||
const { isChrome } = browserInfo;
|
||||
|
||||
return (
|
||||
<Styled.Captions isChrome={isChrome}>
|
||||
<Header
|
||||
leftButtonProps={{
|
||||
onClick: () => {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.NONE,
|
||||
});
|
||||
},
|
||||
'aria-label': intl.formatMessage(intlMessages.hide),
|
||||
label: name,
|
||||
}}
|
||||
customRightButton={Service.amICaptionsOwner(ownerId) ? (
|
||||
<span>
|
||||
<Button
|
||||
onClick={dictating
|
||||
? () => Service.stopDictation(locale)
|
||||
: () => Service.startDictation(locale)}
|
||||
label={dictating
|
||||
? intl.formatMessage(intlMessages.dictationStop)
|
||||
: intl.formatMessage(intlMessages.dictationStart)}
|
||||
aria-describedby="dictationBtnDesc"
|
||||
color={dictating ? 'danger' : 'primary'}
|
||||
disabled={!dictation}
|
||||
/>
|
||||
<div id="dictationBtnDesc" hidden>
|
||||
{dictating
|
||||
? intl.formatMessage(intlMessages.dictationOffDesc)
|
||||
: intl.formatMessage(intlMessages.dictationOnDesc)}
|
||||
</div>
|
||||
</span>
|
||||
) : (
|
||||
<Button
|
||||
color="primary"
|
||||
tooltipLabel={intl.formatMessage(intlMessages.takeOwnershipTooltip, { 0: name })}
|
||||
onClick={() => Service.updateCaptionsOwner(locale, name)}
|
||||
aria-label={intl.formatMessage(intlMessages.takeOwnership)}
|
||||
label={intl.formatMessage(intlMessages.takeOwnership)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<PadContainer
|
||||
externalId={locale}
|
||||
hasPermission={hasPermission}
|
||||
isResizing={isResizing}
|
||||
isRTL={isRTL}
|
||||
/>
|
||||
</Styled.Captions>
|
||||
);
|
||||
};
|
||||
|
||||
Captions.propTypes = propTypes;
|
||||
|
||||
export default injectWbResizeEvent(injectIntl(Captions));
|
@ -1,50 +0,0 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Service from '/imports/ui/components/captions/service';
|
||||
import Captions from './component';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { layoutSelectInput, layoutDispatch } from '../layout/context';
|
||||
import { ACTIONS, PANELS } from '/imports/ui/components/layout/enums';
|
||||
|
||||
const Container = (props) => {
|
||||
const cameraDock = layoutSelectInput((i) => i.cameraDock);
|
||||
const { isResizing } = cameraDock;
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
|
||||
const { amIModerator } = props;
|
||||
|
||||
if (!amIModerator) {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.NONE,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Captions {...{ layoutContextDispatch, isResizing, ...props }} />;
|
||||
};
|
||||
|
||||
export default withTracker(({ amIModerator }) => {
|
||||
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
|
||||
const {
|
||||
locale,
|
||||
name,
|
||||
ownerId,
|
||||
dictating,
|
||||
} = Service.getCaptions();
|
||||
|
||||
return {
|
||||
locale,
|
||||
name,
|
||||
ownerId,
|
||||
dictation: Service.canIDictateThisPad(ownerId),
|
||||
dictating,
|
||||
currentUserId: Auth.userID,
|
||||
isRTL,
|
||||
hasPermission: Service.hasPermission(amIModerator),
|
||||
};
|
||||
})(Container);
|
@ -1,89 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Service from '/imports/ui/components/captions/service';
|
||||
|
||||
const CAPTIONS_CONFIG = window.meetingClientSettings.public.captions;
|
||||
|
||||
class LiveCaptions extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { clear: true };
|
||||
this.timer = null;
|
||||
this.settings = Service.getCaptionsSettings();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { clear } = this.state;
|
||||
|
||||
if (clear) {
|
||||
const { data } = this.props;
|
||||
if (prevProps.data !== data) {
|
||||
// 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 { data } = this.props;
|
||||
const { clear } = this.state;
|
||||
const {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontColor,
|
||||
backgroundColor,
|
||||
} = this.settings;
|
||||
|
||||
const captionStyles = {
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordWrap: 'break-word',
|
||||
fontFamily,
|
||||
fontSize,
|
||||
background: backgroundColor,
|
||||
color: fontColor,
|
||||
};
|
||||
|
||||
const visuallyHidden = {
|
||||
position: 'absolute',
|
||||
overflow: 'hidden',
|
||||
clip: 'rect(0 0 0 0)',
|
||||
height: '1px',
|
||||
width: '1px',
|
||||
margin: '-1px',
|
||||
padding: '0',
|
||||
border: '0',
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-test="liveCaptions" style={captionStyles}>
|
||||
{clear ? '' : data}
|
||||
</div>
|
||||
<div style={visuallyHidden}>
|
||||
{clear ? '' : data}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LiveCaptions.propTypes = {
|
||||
data: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default LiveCaptions;
|
@ -1,10 +0,0 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Service from '/imports/ui/components/captions/service';
|
||||
import LiveCaptions from './component';
|
||||
|
||||
const Container = (props) => <LiveCaptions {...props} />;
|
||||
|
||||
export default withTracker(() => ({
|
||||
data: Service.getCaptionsData(),
|
||||
}))(Container);
|
@ -1,25 +0,0 @@
|
||||
import React from "react";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
import Styled from './styles';
|
||||
|
||||
const ColorPicker = ({ color, onChange, presetColors, colorNames }) => {
|
||||
return (
|
||||
<Styled.Picker>
|
||||
<Styled.ColorPickerGlobalStyle />
|
||||
<HexColorPicker color={color} onChange={onChange} />
|
||||
|
||||
<div>
|
||||
{presetColors.map((presetColor) => (
|
||||
<Styled.PickerSwatch
|
||||
key={presetColor}
|
||||
style={{ background: presetColor }}
|
||||
onClick={() => onChange(presetColor)}
|
||||
title={colorNames[presetColor]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Styled.Picker>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorPicker;
|
@ -1,38 +0,0 @@
|
||||
import styled, { createGlobalStyle } from 'styled-components';
|
||||
|
||||
const ColorPickerGlobalStyle = createGlobalStyle`
|
||||
.react-colorful {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const Picker = styled.div`
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
width: 140px;
|
||||
box-shadow: rgba(0, 0, 0, 0.15) 0px 3px 12px;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const PickerSwatch = styled.button`
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
border: 1px solid #fff;
|
||||
border-style: inset;
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
ColorPickerGlobalStyle,
|
||||
Picker,
|
||||
PickerSwatch,
|
||||
};
|
||||
|
@ -1,416 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import ColorPicker from "./color-picker/component";
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Styled from './styles';
|
||||
|
||||
const DEFAULT_VALUE = 'select';
|
||||
const DEFAULT_KEY = -1;
|
||||
const DEFAULT_INDEX = 0;
|
||||
const FONT_FAMILIES = ['Arial', 'Calibri', 'Times New Roman', 'Sans-serif'];
|
||||
const FONT_SIZES = ['12px', '14px', '18px', '24px', '32px', '42px'];
|
||||
|
||||
const COLORS = [
|
||||
"#000000", "#7a7a7a",
|
||||
"#ff0000", "#ff8800",
|
||||
"#88ff00", "#ffffff",
|
||||
"#00ffff", "#0000ff",
|
||||
"#8800ff", "#ff00ff"
|
||||
];
|
||||
|
||||
// Used to convert hex values to color names for screen reader aria labels
|
||||
const HEX_COLOR_NAMES = {
|
||||
'#000000': 'Black',
|
||||
'#7a7a7a': 'Grey',
|
||||
'#ff0000': 'Red',
|
||||
'#ff8800': 'Orange',
|
||||
'#88ff00': 'Green',
|
||||
'#ffffff': 'White',
|
||||
'#00ffff': 'Cyan',
|
||||
'#0000ff': 'Blue',
|
||||
'#8800ff': 'Dark violet',
|
||||
'#ff00ff': 'Magenta',
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
closeLabel: {
|
||||
id: 'app.captions.menu.closeLabel',
|
||||
description: 'Label for closing captions menu',
|
||||
},
|
||||
title: {
|
||||
id: 'app.captions.menu.title',
|
||||
description: 'Title for the closed captions menu',
|
||||
},
|
||||
start: {
|
||||
id: 'app.captions.menu.start',
|
||||
description: 'Write closed captions',
|
||||
},
|
||||
select: {
|
||||
id: 'app.captions.menu.select',
|
||||
description: 'Select closed captions available language',
|
||||
},
|
||||
backgroundColor: {
|
||||
id: 'app.captions.menu.backgroundColor',
|
||||
description: 'Select closed captions background color',
|
||||
},
|
||||
fontColor: {
|
||||
id: 'app.captions.menu.fontColor',
|
||||
description: 'Select closed captions font color',
|
||||
},
|
||||
fontFamily: {
|
||||
id: 'app.captions.menu.fontFamily',
|
||||
description: 'Select closed captions font family',
|
||||
},
|
||||
fontSize: {
|
||||
id: 'app.captions.menu.fontSize',
|
||||
description: 'Select closed captions font size',
|
||||
},
|
||||
cancelLabel: {
|
||||
id: 'app.captions.menu.cancelLabel',
|
||||
description: 'Cancel button label',
|
||||
},
|
||||
preview: {
|
||||
id: 'app.captions.menu.previewLabel',
|
||||
description: 'Preview area label',
|
||||
},
|
||||
ariaSelectLang: {
|
||||
id: 'app.captions.menu.ariaSelect',
|
||||
description: 'Captions language select aria label',
|
||||
},
|
||||
captionsLabel: {
|
||||
id: 'app.captions.label',
|
||||
description: 'Used in font / size aria labels',
|
||||
},
|
||||
current: {
|
||||
id: 'app.submenu.application.currentSize',
|
||||
description: 'Used in text / background color aria labels',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
activateCaptions: PropTypes.func.isRequired,
|
||||
getCaptionsSettings: PropTypes.func.isRequired,
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
ownedLocales: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
class ReaderMenu extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const {
|
||||
backgroundColor,
|
||||
fontColor,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
} = props.getCaptionsSettings();
|
||||
|
||||
const { ownedLocales } = this.props;
|
||||
|
||||
this.state = {
|
||||
locale: (ownedLocales && ownedLocales[0]) ? ownedLocales[0].locale : null,
|
||||
backgroundColor,
|
||||
fontColor,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
displayBackgroundColorPicker: false,
|
||||
displayFontColorPicker: false,
|
||||
};
|
||||
|
||||
this.handleSelectChange = this.handleSelectChange.bind(this);
|
||||
this.handleColorPickerClick = this.handleColorPickerClick.bind(this);
|
||||
this.handleCloseColorPicker = this.handleCloseColorPicker.bind(this);
|
||||
this.handleFontColorChange = this.handleFontColorChange.bind(this);
|
||||
this.handleBackgroundColorChange = this.handleBackgroundColorChange.bind(this);
|
||||
this.handleLocaleChange = this.handleLocaleChange.bind(this);
|
||||
this.handleStart = this.handleStart.bind(this);
|
||||
this.getPreviewStyle = this.getPreviewStyle.bind(this);
|
||||
}
|
||||
|
||||
handleColorPickerClick(fieldname) {
|
||||
const obj = {};
|
||||
// eslint-disable-next-line react/destructuring-assignment
|
||||
obj[fieldname] = !this.state[fieldname];
|
||||
this.setState(obj);
|
||||
}
|
||||
|
||||
handleCloseColorPicker() {
|
||||
this.setState({
|
||||
displayBackgroundColorPicker: false,
|
||||
displayFontColorPicker: false,
|
||||
});
|
||||
}
|
||||
|
||||
handleFontColorChange(color) {
|
||||
this.setState({ fontColor: color });
|
||||
this.handleCloseColorPicker();
|
||||
}
|
||||
|
||||
handleBackgroundColorChange(color) {
|
||||
this.setState({ backgroundColor: color });
|
||||
this.handleCloseColorPicker();
|
||||
}
|
||||
|
||||
handleLocaleChange(event) {
|
||||
this.setState({ locale: event.target.value });
|
||||
}
|
||||
|
||||
|
||||
handleSelectChange(fieldname, options, event) {
|
||||
const obj = {};
|
||||
obj[fieldname] = options[event.target.value];
|
||||
this.setState(obj);
|
||||
}
|
||||
|
||||
handleStart() {
|
||||
const { closeModal, activateCaptions } = this.props;
|
||||
const {
|
||||
locale,
|
||||
backgroundColor,
|
||||
fontColor,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
} = this.state;
|
||||
const settings = {
|
||||
backgroundColor,
|
||||
fontColor,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
};
|
||||
activateCaptions(locale, settings);
|
||||
closeModal();
|
||||
}
|
||||
|
||||
getPreviewStyle() {
|
||||
const {
|
||||
backgroundColor,
|
||||
fontColor,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
} = this.state;
|
||||
|
||||
return {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
color: fontColor,
|
||||
background: backgroundColor,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
ownedLocales,
|
||||
closeModal,
|
||||
isOpen,
|
||||
priority,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
backgroundColor,
|
||||
displayBackgroundColorPicker,
|
||||
displayFontColorPicker,
|
||||
fontColor,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
locale,
|
||||
} = this.state;
|
||||
|
||||
const defaultLocale = locale || DEFAULT_VALUE;
|
||||
|
||||
const ariaTextColor = `${intl.formatMessage(intlMessages.fontColor)} ${intl.formatMessage(intlMessages.current, { 0: HEX_COLOR_NAMES[fontColor.toLowerCase()] })}`;
|
||||
const ariaBackgroundColor = `${intl.formatMessage(intlMessages.backgroundColor)} ${intl.formatMessage(intlMessages.current, { 0: HEX_COLOR_NAMES[backgroundColor.toLowerCase()] })}`;
|
||||
const ariaFont = `${intl.formatMessage(intlMessages.captionsLabel)} ${intl.formatMessage(intlMessages.fontFamily)}`;
|
||||
const ariaSize = `${intl.formatMessage(intlMessages.captionsLabel)} ${intl.formatMessage(intlMessages.fontSize)}`;
|
||||
|
||||
return (
|
||||
<Styled.ReaderMenuModal
|
||||
onRequestClose={closeModal}
|
||||
hideBorder
|
||||
contentLabel={intl.formatMessage(intlMessages.title)}
|
||||
{...{
|
||||
isOpen,
|
||||
priority,
|
||||
}}
|
||||
>
|
||||
<Styled.Title>
|
||||
{intl.formatMessage(intlMessages.title)}
|
||||
</Styled.Title>
|
||||
{!locale ? null : (
|
||||
<div>
|
||||
<Styled.Col>
|
||||
<Styled.Row>
|
||||
<Styled.Label aria-hidden>
|
||||
{intl.formatMessage(intlMessages.ariaSelectLang)}
|
||||
</Styled.Label>
|
||||
<Styled.Select
|
||||
aria-label={intl.formatMessage(intlMessages.ariaSelectLang)}
|
||||
onChange={this.handleLocaleChange}
|
||||
defaultValue={defaultLocale}
|
||||
lang={locale}
|
||||
>
|
||||
<option
|
||||
disabled
|
||||
key={DEFAULT_KEY}
|
||||
value={DEFAULT_VALUE}
|
||||
>
|
||||
{intl.formatMessage(intlMessages.select)}
|
||||
</option>
|
||||
{ownedLocales.map((loc) => (
|
||||
<option
|
||||
key={loc.locale}
|
||||
value={loc.locale}
|
||||
lang={loc.locale}
|
||||
>
|
||||
{loc.name}
|
||||
</option>
|
||||
))}
|
||||
</Styled.Select>
|
||||
</Styled.Row>
|
||||
|
||||
<Styled.Row>
|
||||
<Styled.Label aria-hidden>
|
||||
{intl.formatMessage(intlMessages.fontColor)}
|
||||
</Styled.Label>
|
||||
<Styled.Swatch
|
||||
aria-label={ariaTextColor}
|
||||
tabIndex={DEFAULT_INDEX}
|
||||
onClick={this.handleColorPickerClick.bind(this, 'displayFontColorPicker')}
|
||||
onKeyPress={() => { }}
|
||||
role="button"
|
||||
>
|
||||
<Styled.SwatchInner style={{ background: fontColor }} />
|
||||
</Styled.Swatch>
|
||||
{
|
||||
displayFontColorPicker
|
||||
? (
|
||||
<Styled.ColorPickerPopover>
|
||||
<Styled.ColorPickerOverlay
|
||||
onClick={this.handleCloseColorPicker.bind(this)}
|
||||
onKeyPress={() => { }}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={ariaTextColor}
|
||||
/>
|
||||
<ColorPicker
|
||||
color={fontColor}
|
||||
onChange={this.handleFontColorChange}
|
||||
presetColors={COLORS}
|
||||
colorNames={HEX_COLOR_NAMES}
|
||||
/>
|
||||
</Styled.ColorPickerPopover>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</Styled.Row>
|
||||
|
||||
<Styled.Row>
|
||||
<Styled.Label aria-hidden>
|
||||
{intl.formatMessage(intlMessages.backgroundColor)}
|
||||
</Styled.Label>
|
||||
<Styled.Swatch
|
||||
aria-label={ariaBackgroundColor}
|
||||
tabIndex={DEFAULT_INDEX}
|
||||
onClick={this.handleColorPickerClick.bind(this, 'displayBackgroundColorPicker')}
|
||||
role="button"
|
||||
onKeyPress={() => { }}
|
||||
>
|
||||
<Styled.SwatchInner style={{ background: backgroundColor }} />
|
||||
</Styled.Swatch>
|
||||
{
|
||||
displayBackgroundColorPicker
|
||||
? (
|
||||
<Styled.ColorPickerPopover>
|
||||
<Styled.ColorPickerOverlay
|
||||
aria-label={ariaBackgroundColor}
|
||||
onClick={this.handleCloseColorPicker.bind(this)}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onKeyPress={() => { }}
|
||||
/>
|
||||
<ColorPicker
|
||||
color={fontColor}
|
||||
onChange={this.handleBackgroundColorChange}
|
||||
presetColors={COLORS}
|
||||
colorNames={HEX_COLOR_NAMES}
|
||||
/>
|
||||
</Styled.ColorPickerPopover>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</Styled.Row>
|
||||
|
||||
<Styled.Row>
|
||||
<Styled.Label aria-hidden>
|
||||
{intl.formatMessage(intlMessages.fontFamily)}
|
||||
</Styled.Label>
|
||||
<Styled.Select
|
||||
aria-label={ariaFont}
|
||||
defaultValue={FONT_FAMILIES.indexOf(fontFamily)}
|
||||
onChange={this.handleSelectChange.bind(this, 'fontFamily', FONT_FAMILIES)}
|
||||
>
|
||||
{FONT_FAMILIES.map((family, index) => (
|
||||
<option
|
||||
key={family}
|
||||
value={index}
|
||||
>
|
||||
{family}
|
||||
</option>
|
||||
))}
|
||||
</Styled.Select>
|
||||
</Styled.Row>
|
||||
|
||||
<Styled.Row>
|
||||
<Styled.Label aria-hidden>
|
||||
{intl.formatMessage(intlMessages.fontSize)}
|
||||
</Styled.Label>
|
||||
<Styled.Select
|
||||
aria-label={ariaSize}
|
||||
defaultValue={FONT_SIZES.indexOf(fontSize)}
|
||||
onChange={this.handleSelectChange.bind(this, 'fontSize', FONT_SIZES)}
|
||||
>
|
||||
{FONT_SIZES.map((size, index) => (
|
||||
<option
|
||||
key={size}
|
||||
value={index}
|
||||
>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
</Styled.Select>
|
||||
</Styled.Row>
|
||||
|
||||
<Styled.Row>
|
||||
<Styled.Label>{intl.formatMessage(intlMessages.preview)}</Styled.Label>
|
||||
<span aria-hidden style={this.getPreviewStyle()}>AaBbCc</span>
|
||||
</Styled.Row>
|
||||
</Styled.Col>
|
||||
</div>
|
||||
)}
|
||||
<Styled.Footer>
|
||||
<Styled.Actions>
|
||||
<Button
|
||||
label={intl.formatMessage(intlMessages.cancelLabel)}
|
||||
onClick={closeModal}
|
||||
/>
|
||||
<Button
|
||||
color="primary"
|
||||
label={intl.formatMessage(intlMessages.start)}
|
||||
onClick={() => this.handleStart()}
|
||||
disabled={locale == null}
|
||||
data-test="startViewingClosedCaptions"
|
||||
/>
|
||||
</Styled.Actions>
|
||||
</Styled.Footer>
|
||||
</Styled.ReaderMenuModal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReaderMenu.propTypes = propTypes;
|
||||
|
||||
export default injectIntl(ReaderMenu);
|
@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import ReaderMenu from './component';
|
||||
import CaptionsService from '/imports/ui/components/captions/service';
|
||||
|
||||
const ReaderMenuContainer = (props) => <ReaderMenu {...props} />;
|
||||
|
||||
export default withTracker(({ setIsOpen }) => ({
|
||||
closeModal: () => setIsOpen(false),
|
||||
activateCaptions: (locale, settings) => CaptionsService.activateCaptions(locale, settings),
|
||||
getCaptionsSettings: () => CaptionsService.getCaptionsSettings(),
|
||||
ownedLocales: CaptionsService.getOwnedLocales(),
|
||||
}))(ReaderMenuContainer);
|
@ -1,133 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
|
||||
import ModalSimple from '/imports/ui/components/common/modal/simple/component';
|
||||
import {
|
||||
colorGrayDark,
|
||||
colorWhite,
|
||||
colorGrayLabel,
|
||||
colorGrayLight,
|
||||
colorPrimary,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { borderSize, borderSizeLarge } from '/imports/ui/stylesheets/styled-components/general';
|
||||
|
||||
const ReaderMenuModal = styled(ModalSimple)`
|
||||
padding: 1rem;
|
||||
`;
|
||||
|
||||
const Title = styled.header`
|
||||
display: block;
|
||||
color: ${colorGrayDark};
|
||||
font-size: 1.4rem;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const Col = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
margin: 0 1.5rem 0 0;
|
||||
justify-content: center;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: 0 0 0 1.5rem;
|
||||
}
|
||||
|
||||
@media ${smallOnly} {
|
||||
width: 100%;
|
||||
height: unset;
|
||||
}
|
||||
`;
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: .2rem 0 .2rem 0;
|
||||
`;
|
||||
|
||||
const Label = styled.div`
|
||||
flex: 1 0 0;
|
||||
`;
|
||||
|
||||
const Select = styled.select`
|
||||
background-color: ${colorWhite};
|
||||
border-radius: 0.3rem;
|
||||
color: ${colorGrayLabel};
|
||||
height: 1.6rem;
|
||||
margin-top: 0.4rem;
|
||||
width: 50%;
|
||||
`;
|
||||
|
||||
const Swatch = styled.div`
|
||||
flex: 1 0 0;
|
||||
border-radius: ${borderSize};
|
||||
border: ${borderSize} solid ${colorGrayLight};
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: inset 0 0 0 ${borderSizeLarge} ${colorPrimary};
|
||||
border-radius: ${borderSize};
|
||||
}
|
||||
`;
|
||||
|
||||
const SwatchInner = styled.div`
|
||||
width: auto;
|
||||
height: 1.1rem;
|
||||
border-radius: ${borderSize};
|
||||
`;
|
||||
|
||||
const ColorPickerPopover = styled.div`
|
||||
position: absolute;
|
||||
z-index: 1001;
|
||||
`;
|
||||
|
||||
const ColorPickerOverlay = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
`;
|
||||
|
||||
const Footer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const Actions = styled.div`
|
||||
margin-left: auto;
|
||||
margin-right: 3px;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin-right: auto;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
> * {
|
||||
&:first-child {
|
||||
margin-right: 3px;
|
||||
margin-left: inherit;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin-right: inherit;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
ReaderMenuModal,
|
||||
Title,
|
||||
Col,
|
||||
Row,
|
||||
Label,
|
||||
Select,
|
||||
Swatch,
|
||||
SwatchInner,
|
||||
ColorPickerPopover,
|
||||
ColorPickerOverlay,
|
||||
Footer,
|
||||
Actions,
|
||||
};
|
@ -1,245 +0,0 @@
|
||||
import Captions from '/imports/api/captions';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import PadsService from '/imports/ui/components/pads/service';
|
||||
import SpeechService from '/imports/ui/components/captions/speech/service';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Session } from 'meteor/session';
|
||||
import { isCaptionsEnabled } from '/imports/ui/services/features';
|
||||
|
||||
const CAPTIONS_CONFIG = window.meetingClientSettings.public.captions;
|
||||
const LINE_BREAK = '\n';
|
||||
|
||||
const getAvailableLocales = () => {
|
||||
const availableLocales = Captions.find(
|
||||
{ meetingId: Auth.meetingID, ownerId: '' },
|
||||
{ sort: { locale: 1 } },
|
||||
{ fields: { ownerId: 1, locale: 1, name: 1 } },
|
||||
).fetch();
|
||||
|
||||
return availableLocales;
|
||||
};
|
||||
|
||||
const getOwnedLocales = () => {
|
||||
const ownedLocales = Captions.find(
|
||||
{ meetingId: Auth.meetingID, ownerId: { $not: '' } },
|
||||
{ fields: { ownerId: 1, locale: 1, name: 1 } },
|
||||
).fetch();
|
||||
|
||||
return ownedLocales;
|
||||
};
|
||||
|
||||
const updateCaptionsOwner = (locale, name) => makeCall('updateCaptionsOwner', locale, name);
|
||||
|
||||
const startDictation = (locale) => makeCall('startDictation', locale);
|
||||
|
||||
const stopDictation = (locale) => makeCall('stopDictation', locale);
|
||||
|
||||
const getCaptionsSettings = () => {
|
||||
const settings = Session.get('captionsSettings');
|
||||
if (settings) return settings;
|
||||
|
||||
const {
|
||||
background,
|
||||
font,
|
||||
} = CAPTIONS_CONFIG;
|
||||
|
||||
return {
|
||||
backgroundColor: background,
|
||||
fontColor: font.color,
|
||||
fontFamily: font.family,
|
||||
fontSize: font.size,
|
||||
};
|
||||
};
|
||||
|
||||
const setCaptionsSettings = (settings) => Session.set('captionsSettings', settings);
|
||||
|
||||
const getCaptionsLocale = () => Session.get('captionsLocale') || '';
|
||||
|
||||
const getCaptions = () => {
|
||||
const locale = getCaptionsLocale();
|
||||
if (locale) {
|
||||
const {
|
||||
name,
|
||||
ownerId,
|
||||
dictating,
|
||||
} = Captions.findOne({
|
||||
meetingId: Auth.meetingID,
|
||||
locale,
|
||||
});
|
||||
|
||||
return {
|
||||
locale,
|
||||
name,
|
||||
ownerId,
|
||||
dictating,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
name: '',
|
||||
ownerId: '',
|
||||
dictating: false,
|
||||
};
|
||||
};
|
||||
|
||||
const setCaptionsLocale = (locale) => Session.set('captionsLocale', locale);
|
||||
|
||||
const getCaptionsActive = () => Session.get('captionsActive') || '';
|
||||
|
||||
const formatCaptionsText = (text) => {
|
||||
const splitText = text.split(LINE_BREAK);
|
||||
const filteredText = splitText.filter((line, index) => {
|
||||
const lastLine = index === (splitText.length - 1);
|
||||
const emptyLine = line.length === 0;
|
||||
|
||||
return (!emptyLine || lastLine);
|
||||
});
|
||||
|
||||
while (filteredText.length > CAPTIONS_CONFIG.lines) filteredText.shift();
|
||||
|
||||
return filteredText.join(LINE_BREAK);
|
||||
};
|
||||
|
||||
const getCaptionsData = () => {
|
||||
const locale = getCaptionsActive();
|
||||
if (locale) {
|
||||
const captions = Captions.findOne({
|
||||
meetingId: Auth.meetingID,
|
||||
locale,
|
||||
dictating: true,
|
||||
});
|
||||
|
||||
let data = '';
|
||||
if (captions) {
|
||||
data = captions.transcript;
|
||||
} else {
|
||||
data = PadsService.getPadTail(locale);
|
||||
}
|
||||
|
||||
return formatCaptionsText(data);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const setCaptionsActive = (locale) => Session.set('captionsActive', locale);
|
||||
|
||||
const amICaptionsOwner = (ownerId) => ownerId === Auth.userID;
|
||||
|
||||
const isCaptionsAvailable = () => {
|
||||
if (isCaptionsEnabled()) {
|
||||
const ownedLocales = getOwnedLocales();
|
||||
|
||||
return (ownedLocales.length > 0);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const isCaptionsActive = () => {
|
||||
const enabled = isCaptionsEnabled();
|
||||
const activated = getCaptionsActive() !== '';
|
||||
|
||||
return (enabled && activated);
|
||||
};
|
||||
|
||||
const deactivateCaptions = () => setCaptionsActive('');
|
||||
|
||||
const activateCaptions = (locale, settings) => {
|
||||
setCaptionsSettings(settings);
|
||||
setCaptionsActive(locale);
|
||||
};
|
||||
|
||||
const getName = (locale) => {
|
||||
const captions = Captions.findOne({
|
||||
meetingId: Auth.meetingID,
|
||||
locale,
|
||||
});
|
||||
|
||||
return captions.name;
|
||||
};
|
||||
|
||||
const createCaptions = (locale) => {
|
||||
const name = getName(locale);
|
||||
PadsService.createGroup(locale, CAPTIONS_CONFIG.id, name);
|
||||
updateCaptionsOwner(locale, name);
|
||||
setCaptionsLocale(locale);
|
||||
};
|
||||
|
||||
const hasPermission = (isModerator) => {
|
||||
if (isModerator) {
|
||||
const { ownerId } = getCaptions();
|
||||
|
||||
return Auth.userID === ownerId;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const getDictationStatus = (isModerator) => {
|
||||
if (!CAPTIONS_CONFIG.dictation || !isModerator) {
|
||||
return {
|
||||
locale: '',
|
||||
dictating: false,
|
||||
};
|
||||
}
|
||||
|
||||
const captions = Captions.findOne({
|
||||
meetingId: Auth.meetingID,
|
||||
ownerId: Auth.userID,
|
||||
}, {
|
||||
fields: {
|
||||
locale: 1,
|
||||
dictating: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (captions) {
|
||||
return {
|
||||
locale: captions.locale,
|
||||
dictating: captions.dictating,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
locale: '',
|
||||
dictating: false,
|
||||
};
|
||||
};
|
||||
|
||||
const canIDictateThisPad = (ownerId) => {
|
||||
if (!CAPTIONS_CONFIG.dictation) return false;
|
||||
|
||||
if (ownerId !== Auth.userID) return false;
|
||||
|
||||
if (!SpeechService.hasSpeechRecognitionSupport()) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export default {
|
||||
ID: CAPTIONS_CONFIG.id,
|
||||
getAvailableLocales,
|
||||
getOwnedLocales,
|
||||
updateCaptionsOwner,
|
||||
startDictation,
|
||||
stopDictation,
|
||||
getCaptionsSettings,
|
||||
getCaptionsData,
|
||||
getCaptions,
|
||||
hasPermission,
|
||||
amICaptionsOwner,
|
||||
isCaptionsEnabled,
|
||||
isCaptionsAvailable,
|
||||
isCaptionsActive,
|
||||
deactivateCaptions,
|
||||
activateCaptions,
|
||||
formatCaptionsText,
|
||||
createCaptions,
|
||||
getCaptionsLocale,
|
||||
setCaptionsLocale,
|
||||
getDictationStatus,
|
||||
canIDictateThisPad,
|
||||
};
|
@ -1,144 +0,0 @@
|
||||
import { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { notify } from '/imports/ui/services/notification';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import CaptionsService from '/imports/ui/components/captions/service';
|
||||
import Service from './service';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
start: {
|
||||
id: 'app.captions.speech.start',
|
||||
description: 'Notification on speech recognition start',
|
||||
},
|
||||
stop: {
|
||||
id: 'app.captions.speech.stop',
|
||||
description: 'Notification on speech recognition stop',
|
||||
},
|
||||
error: {
|
||||
id: 'app.captions.speech.error',
|
||||
description: 'Notification on speech recognition error',
|
||||
},
|
||||
});
|
||||
|
||||
class Speech extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onStop = this.onStop.bind(this);
|
||||
this.onError = this.onError.bind(this);
|
||||
this.onResult = this.onResult.bind(this);
|
||||
|
||||
this.result = {
|
||||
transcript: '',
|
||||
isFinal: true,
|
||||
};
|
||||
|
||||
this.speechRecognition = Service.initSpeechRecognition(props.setUserSpeechLocale);
|
||||
|
||||
if (this.speechRecognition) {
|
||||
this.speechRecognition.onstart = () => notify(props.intl.formatMessage(intlMessages.start), 'info', 'closed_caption');
|
||||
this.speechRecognition.onend = () => notify(props.intl.formatMessage(intlMessages.stop), 'info', 'closed_caption');
|
||||
this.speechRecognition.onerror = (event) => this.onError(event);
|
||||
this.speechRecognition.onresult = (event) => this.onResult(event);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
locale,
|
||||
dictating,
|
||||
} = this.props;
|
||||
|
||||
// Start dictating
|
||||
if (!prevProps.dictating && dictating) {
|
||||
if (this.speechRecognition) {
|
||||
this.speechRecognition.lang = locale;
|
||||
try {
|
||||
this.speechRecognition.start();
|
||||
} catch (event) {
|
||||
this.onError(event.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop dictating
|
||||
if (prevProps.dictating && !dictating) {
|
||||
this.onStop();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.onStop();
|
||||
}
|
||||
|
||||
onError(error) {
|
||||
this.onStop();
|
||||
|
||||
const {
|
||||
intl,
|
||||
locale,
|
||||
} = this.props;
|
||||
|
||||
notify(intl.formatMessage(intlMessages.error), 'error', 'warning');
|
||||
CaptionsService.stopDictation(locale);
|
||||
logger.error({
|
||||
logCode: 'captions_speech_recognition',
|
||||
extraInfo: { error },
|
||||
}, 'Captions speech recognition error');
|
||||
}
|
||||
|
||||
onStop() {
|
||||
const { locale } = this.props;
|
||||
|
||||
if (this.speechRecognition) {
|
||||
const {
|
||||
isFinal,
|
||||
transcript,
|
||||
} = this.result;
|
||||
|
||||
if (!isFinal) {
|
||||
Service.pushFinalTranscript(locale, transcript);
|
||||
this.speechRecognition.abort();
|
||||
} else {
|
||||
this.speechRecognition.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onResult(event) {
|
||||
const { locale } = this.props;
|
||||
|
||||
const {
|
||||
resultIndex,
|
||||
results,
|
||||
} = event;
|
||||
|
||||
const { transcript } = results[resultIndex][0];
|
||||
const { isFinal } = results[resultIndex];
|
||||
|
||||
this.result.transcript = transcript;
|
||||
this.result.isFinal = isFinal;
|
||||
|
||||
if (isFinal) {
|
||||
Service.pushFinalTranscript(locale, transcript);
|
||||
} else {
|
||||
Service.pushInterimTranscript(locale, transcript);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Speech.propTypes = {
|
||||
locale: PropTypes.string.isRequired,
|
||||
dictating: PropTypes.bool.isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
setUserSpeechLocale: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(Speech);
|
@ -1,33 +0,0 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import CaptionsService from '/imports/ui/components/captions/service';
|
||||
import Speech from './component';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { SET_SPEECH_LOCALE } from '/imports/ui/core/graphql/mutations/userMutations';
|
||||
|
||||
const Container = (props) => {
|
||||
const [setSpeechLocale] = useMutation(SET_SPEECH_LOCALE);
|
||||
|
||||
const setUserSpeechLocale = (locale, provider) => {
|
||||
setSpeechLocale({
|
||||
variables: {
|
||||
locale,
|
||||
provider,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return <Speech setUserSpeechLocale={setUserSpeechLocale} {...props} />;
|
||||
};
|
||||
|
||||
export default withTracker(({ isModerator }) => {
|
||||
const {
|
||||
locale,
|
||||
dictating,
|
||||
} = CaptionsService.getDictationStatus(isModerator);
|
||||
|
||||
return {
|
||||
locale,
|
||||
dictating,
|
||||
};
|
||||
})(Container);
|
@ -1,43 +0,0 @@
|
||||
import { throttle } from '/imports/utils/throttle';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
|
||||
const DEFAULT_LANGUAGE = 'en-US';
|
||||
const THROTTLE_TIMEOUT = 2000;
|
||||
|
||||
const SpeechRecognitionAPI = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
|
||||
const hasSpeechRecognitionSupport = () => typeof SpeechRecognitionAPI !== 'undefined';
|
||||
|
||||
const initSpeechRecognition = (locale = DEFAULT_LANGUAGE) => {
|
||||
if (hasSpeechRecognitionSupport()) {
|
||||
const speechRecognition = new SpeechRecognitionAPI();
|
||||
speechRecognition.continuous = true;
|
||||
speechRecognition.interimResults = true;
|
||||
speechRecognition.lang = locale;
|
||||
|
||||
return speechRecognition;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const pushSpeechTranscript = (locale, transcript, type) => makeCall('pushSpeechTranscript', locale, transcript, type);
|
||||
|
||||
const throttledTranscriptPush = throttle(pushSpeechTranscript, THROTTLE_TIMEOUT, {
|
||||
leading: false,
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
const pushInterimTranscript = (locale, transcript) => throttledTranscriptPush(locale, transcript, 'interim');
|
||||
|
||||
const pushFinalTranscript = (locale, transcript) => {
|
||||
throttledTranscriptPush.cancel();
|
||||
pushSpeechTranscript(locale, transcript, 'final');
|
||||
};
|
||||
|
||||
export default {
|
||||
hasSpeechRecognitionSupport,
|
||||
initSpeechRecognition,
|
||||
pushInterimTranscript,
|
||||
pushFinalTranscript,
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { smPaddingX } from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
|
||||
|
||||
const Captions = styled.div`
|
||||
background-color: ${colorWhite};
|
||||
padding: ${smPaddingX};
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
${({ isChrome }) => isChrome && `
|
||||
transform: translateZ(0);
|
||||
`}
|
||||
|
||||
@media ${smallOnly} {
|
||||
transform: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default { Captions };
|
@ -1,161 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import Service from '/imports/ui/components/captions/service';
|
||||
import LocalesDropdown from '/imports/ui/components/common/locales-dropdown/component';
|
||||
import Styled from './styles';
|
||||
import { PANELS, ACTIONS } from '../../layout/enums';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
closeLabel: {
|
||||
id: 'app.captions.menu.closeLabel',
|
||||
description: 'Label for closing captions menu',
|
||||
},
|
||||
title: {
|
||||
id: 'app.captions.menu.title',
|
||||
description: 'Title for the closed captions menu',
|
||||
},
|
||||
subtitle: {
|
||||
id: 'app.captions.menu.subtitle',
|
||||
description: 'Subtitle for the closed captions writer menu',
|
||||
},
|
||||
start: {
|
||||
id: 'app.captions.menu.start',
|
||||
description: 'Write closed captions',
|
||||
},
|
||||
ariaStart: {
|
||||
id: 'app.captions.menu.ariaStart',
|
||||
description: 'aria label for start captions button',
|
||||
},
|
||||
ariaStartDesc: {
|
||||
id: 'app.captions.menu.ariaStartDesc',
|
||||
description: 'aria description for start captions button',
|
||||
},
|
||||
select: {
|
||||
id: 'app.captions.menu.select',
|
||||
description: 'Select closed captions available language',
|
||||
},
|
||||
ariaSelect: {
|
||||
id: 'app.captions.menu.ariaSelect',
|
||||
description: 'Aria label for captions language selector',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
availableLocales: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
class WriterMenu extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { availableLocales, intl } = this.props;
|
||||
|
||||
const candidate = availableLocales.filter(
|
||||
(l) => l.locale.substring(0, 2) === intl.locale.substring(0, 2),
|
||||
);
|
||||
|
||||
this.state = {
|
||||
locale: candidate && candidate[0] ? candidate[0].locale : null,
|
||||
};
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleStart = this.handleStart.bind(this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { setIsOpen } = this.props;
|
||||
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
this.setState({ locale: event.target.value });
|
||||
}
|
||||
|
||||
handleStart() {
|
||||
const {
|
||||
setIsOpen,
|
||||
layoutContextDispatch,
|
||||
} = this.props;
|
||||
|
||||
const { locale } = this.state;
|
||||
Service.createCaptions(locale);
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.CAPTIONS,
|
||||
});
|
||||
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
availableLocales,
|
||||
isOpen,
|
||||
onRequestClose,
|
||||
priority,
|
||||
setIsOpen
|
||||
|
||||
} = this.props;
|
||||
|
||||
const { locale } = this.state;
|
||||
|
||||
return (
|
||||
<Styled.WriterMenuModal
|
||||
hideBorder
|
||||
contentLabel={intl.formatMessage(intlMessages.title)}
|
||||
title={intl.formatMessage(intlMessages.title)}
|
||||
{...{
|
||||
isOpen,
|
||||
onRequestClose,
|
||||
priority,
|
||||
setIsOpen
|
||||
}}
|
||||
>
|
||||
<Styled.Content>
|
||||
<span>
|
||||
{intl.formatMessage(intlMessages.subtitle)}
|
||||
</span>
|
||||
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||
<label
|
||||
aria-hidden
|
||||
htmlFor="captionsLangSelector"
|
||||
aria-label={intl.formatMessage(intlMessages.ariaSelect)}
|
||||
/>
|
||||
|
||||
<Styled.WriterMenuSelect>
|
||||
<LocalesDropdown
|
||||
allLocales={availableLocales}
|
||||
handleChange={this.handleChange}
|
||||
value={locale}
|
||||
elementId="captionsLangSelector"
|
||||
selectMessage={intl.formatMessage(intlMessages.select)}
|
||||
/>
|
||||
</Styled.WriterMenuSelect>
|
||||
<Styled.StartBtn
|
||||
label={intl.formatMessage(intlMessages.start)}
|
||||
aria-label={intl.formatMessage(intlMessages.ariaStart)}
|
||||
aria-describedby="descriptionStart"
|
||||
onClick={this.handleStart}
|
||||
disabled={locale == null}
|
||||
data-test="startWritingClosedCaptions"
|
||||
/>
|
||||
<div id="descriptionStart" hidden>{intl.formatMessage(intlMessages.ariaStartDesc)}</div>
|
||||
</Styled.Content>
|
||||
</Styled.WriterMenuModal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WriterMenu.propTypes = propTypes;
|
||||
|
||||
export default injectIntl(WriterMenu);
|
@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Service from '/imports/ui/components/captions/service';
|
||||
import WriterMenu from './component';
|
||||
import { layoutDispatch } from '../../layout/context';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
|
||||
const WriterMenuContainer = (props) => {
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
|
||||
const { data: currentUserData } = useCurrentUser((user) => ({
|
||||
isModerator: user.isModerator,
|
||||
}));
|
||||
const amIModerator = currentUserData?.isModerator;
|
||||
|
||||
return amIModerator && <WriterMenu {...{ layoutContextDispatch, ...props }} />;
|
||||
};
|
||||
|
||||
export default withTracker(({ setIsOpen }) => ({
|
||||
closeModal: () => setIsOpen(false),
|
||||
availableLocales: Service.getAvailableLocales(),
|
||||
}))(WriterMenuContainer);
|
@ -1,81 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
borderSize,
|
||||
borderSizeLarge,
|
||||
mdPaddingX,
|
||||
} from '/imports/ui/stylesheets/styled-components/general';
|
||||
import {
|
||||
colorWhite,
|
||||
colorLink,
|
||||
colorGrayLighter,
|
||||
colorGrayLabel,
|
||||
colorPrimary,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import ModalSimple from '/imports/ui/components/common/modal/simple/component';
|
||||
|
||||
const WriterMenuModal = styled(ModalSimple)`
|
||||
min-height: 20rem;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: .3rem 0 0.5rem 0;
|
||||
`;
|
||||
|
||||
const StartBtn = styled(Button)`
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: ${mdPaddingX};
|
||||
color: ${colorWhite} !important;
|
||||
background-color: ${colorLink} !important;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
& > i {
|
||||
color: #3c5764;
|
||||
}
|
||||
`;
|
||||
|
||||
const WriterMenuSelect = styled.div`
|
||||
width: 40%;
|
||||
|
||||
& > select {
|
||||
background-color: ${colorWhite};
|
||||
border: ${borderSize} solid ${colorWhite};
|
||||
border-radius: ${borderSize};
|
||||
border-bottom: 0.1rem solid ${colorGrayLighter};
|
||||
color: ${colorGrayLabel};
|
||||
width: 100%;
|
||||
height: 1.75rem;
|
||||
padding: 1px;
|
||||
|
||||
&:hover {
|
||||
outline: transparent;
|
||||
outline-style: dotted;
|
||||
outline-width: ${borderSize};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: inset 0 0 0 ${borderSizeLarge} ${colorPrimary};
|
||||
border-radius: ${borderSize};
|
||||
outline: transparent;
|
||||
outline-width: ${borderSize};
|
||||
outline-style: solid;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
WriterMenuModal,
|
||||
Content,
|
||||
StartBtn,
|
||||
WriterMenuSelect,
|
||||
};
|
@ -12,8 +12,6 @@ import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
|
||||
import { UI_DATA_LISTENER_SUBSCRIBED } from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-data-hooks/consts';
|
||||
import { ExternalVideoVolumeUiDataNames } from 'bigbluebutton-html-plugin-sdk';
|
||||
import { ExternalVideoVolumeUiDataPayloads } from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-data-hooks/external-video/volume/types';
|
||||
import MediaService from '/imports/ui/components/media/service';
|
||||
import NotesService from '/imports/ui/components/notes/service';
|
||||
|
||||
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
||||
import {
|
||||
@ -74,8 +72,6 @@ interface ExternalVideoPlayerProps {
|
||||
currentTime: number;
|
||||
key: string;
|
||||
setKey: (key: string) => void;
|
||||
shouldShowSharedNotes(): boolean;
|
||||
pinSharedNotes(pinned: boolean): void;
|
||||
}
|
||||
|
||||
// @ts-ignore - PeerTubePlayer is not typed
|
||||
@ -97,8 +93,6 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
|
||||
isEchoTest,
|
||||
key,
|
||||
setKey,
|
||||
shouldShowSharedNotes,
|
||||
pinSharedNotes,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
@ -225,7 +219,7 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
const unsynchedPlayer = reactPlayerState !== playing;
|
||||
if (unsynchedPlayer) {
|
||||
if (unsynchedPlayer && !!videoUrl) {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setShowUnsynchedMsg(true);
|
||||
}, AUTO_PLAY_BLOCK_DETECTION_TIMEOUT_SECONDS * 1000);
|
||||
@ -257,16 +251,6 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
|
||||
}
|
||||
}, [playerRef.current]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldShowSharedNotes()) {
|
||||
pinSharedNotes(false);
|
||||
return () => {
|
||||
pinSharedNotes(true);
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
// --- Plugin related code ---;
|
||||
const internalPlayer = playerRef.current?.getInternalPlayer ? playerRef.current?.getInternalPlayer() : null;
|
||||
if (internalPlayer && internalPlayer?.isMuted
|
||||
@ -539,8 +523,6 @@ const ExternalVideoPlayerContainer: React.FC = () => {
|
||||
currentTime={isPresenter ? playerCurrentTime : currentTime}
|
||||
key={key}
|
||||
setKey={setKey}
|
||||
shouldShowSharedNotes={MediaService.shouldShowSharedNotes}
|
||||
pinSharedNotes={NotesService.pinSharedNotes}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,11 +1,16 @@
|
||||
import React, { useEffect, useReducer, useRef } from 'react';
|
||||
import { createContext, useContextSelector } from 'use-context-selector';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSubscription } from '@apollo/client';
|
||||
import { equals } from 'ramda';
|
||||
import { ACTIONS, PRESENTATION_AREA } from '/imports/ui/components/layout/enums';
|
||||
import { PINNED_PAD_SUBSCRIPTION } from '/imports/ui/components/notes/notes-graphql/queries';
|
||||
import {
|
||||
ACTIONS, PRESENTATION_AREA, PANELS, LAYOUT_TYPE,
|
||||
} from '/imports/ui/components/layout/enums';
|
||||
import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
|
||||
import { INITIAL_INPUT_STATE, INITIAL_OUTPUT_STATE } from './initState';
|
||||
import useUpdatePresentationAreaContentForPlugin from '/imports/ui/components/plugins-engine/ui-data-hooks/layout/presentation-area/utils';
|
||||
import { isPresentationEnabled } from '/imports/ui/services/features';
|
||||
|
||||
// variable to debug in console log
|
||||
const debug = false;
|
||||
@ -37,6 +42,9 @@ const initPresentationAreaContentActions = [{
|
||||
},
|
||||
}];
|
||||
|
||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
||||
|
||||
const initState = {
|
||||
presentationAreaContentActions: initPresentationAreaContentActions,
|
||||
deviceType: null,
|
||||
@ -55,7 +63,6 @@ const initState = {
|
||||
const reducer = (state, action) => {
|
||||
debugActions(action.type, action.value);
|
||||
switch (action.type) {
|
||||
|
||||
case ACTIONS.SET_FOCUSED_CAMERA_ID: {
|
||||
const { cameraDock } = state.input;
|
||||
const { focusedId } = cameraDock;
|
||||
@ -1331,6 +1338,8 @@ const updatePresentationAreaContent = (
|
||||
previousPresentationAreaContentActions,
|
||||
layoutContextDispatch,
|
||||
) => {
|
||||
const { layoutType } = layoutContextState;
|
||||
const { sidebarContent } = layoutContextState.input;
|
||||
const {
|
||||
presentationAreaContentActions: currentPresentationAreaContentActions,
|
||||
} = layoutContextState;
|
||||
@ -1355,6 +1364,32 @@ const updatePresentationAreaContent = (
|
||||
break;
|
||||
}
|
||||
case PRESENTATION_AREA.PINNED_NOTES: {
|
||||
if (
|
||||
(sidebarContent.isOpen || !isPresentationEnabled())
|
||||
&& (sidebarContent.sidebarContentPanel === PANELS.SHARED_NOTES
|
||||
|| !isPresentationEnabled())
|
||||
) {
|
||||
if (layoutType === LAYOUT_TYPE.VIDEO_FOCUS) {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.CHAT,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_ID_CHAT_OPEN,
|
||||
value: PUBLIC_CHAT_ID,
|
||||
});
|
||||
} else {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.NONE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_GENERIC_COMPONENT,
|
||||
value: undefined,
|
||||
@ -1370,6 +1405,10 @@ const updatePresentationAreaContent = (
|
||||
type: ACTIONS.SET_HAS_GENERIC_COMPONENT,
|
||||
value: undefined,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_NOTES_IS_PINNED,
|
||||
value: !lastPresentationContentInPile.value.open,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_EXTERNAL_VIDEO,
|
||||
value: lastPresentationContentInPile.value.open,
|
||||
@ -1381,6 +1420,10 @@ const updatePresentationAreaContent = (
|
||||
type: ACTIONS.SET_HAS_GENERIC_COMPONENT,
|
||||
value: undefined,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_NOTES_IS_PINNED,
|
||||
value: !lastPresentationContentInPile.value.open,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_SCREEN_SHARE,
|
||||
value: lastPresentationContentInPile.value.open,
|
||||
@ -1400,6 +1443,10 @@ const updatePresentationAreaContent = (
|
||||
type: ACTIONS.SET_HAS_GENERIC_COMPONENT,
|
||||
value: undefined,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.PINNED_NOTES,
|
||||
value: !lastPresentationContentInPile.value.open,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@ -1422,6 +1469,8 @@ const LayoutContextProvider = (props) => {
|
||||
},
|
||||
}],
|
||||
);
|
||||
const { data: pinnedPadData } = useSubscription(PINNED_PAD_SUBSCRIPTION);
|
||||
|
||||
const [layoutContextState, layoutContextDispatch] = useReducer(reducer, initState);
|
||||
const { children } = props;
|
||||
useEffect(() => {
|
||||
@ -1431,6 +1480,27 @@ const LayoutContextProvider = (props) => {
|
||||
layoutContextDispatch,
|
||||
);
|
||||
}, [layoutContextState]);
|
||||
useEffect(() => {
|
||||
const isSharedNotesPinned = !!pinnedPadData
|
||||
&& pinnedPadData.sharedNotes[0]?.pinned;
|
||||
if (isSharedNotesPinned) {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PILE_CONTENT_FOR_PRESENTATION_AREA,
|
||||
value: {
|
||||
content: PRESENTATION_AREA.PINNED_NOTES,
|
||||
open: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PILE_CONTENT_FOR_PRESENTATION_AREA,
|
||||
value: {
|
||||
content: PRESENTATION_AREA.PINNED_NOTES,
|
||||
open: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [pinnedPadData]);
|
||||
useUpdatePresentationAreaContentForPlugin(layoutContextState);
|
||||
return (
|
||||
<LayoutContextSelector.Provider value={
|
||||
@ -1446,18 +1516,16 @@ const LayoutContextProvider = (props) => {
|
||||
};
|
||||
LayoutContextProvider.propTypes = providerPropTypes;
|
||||
|
||||
const layoutSelect = (selector) => {
|
||||
return useContextSelector(LayoutContextSelector, layout => selector(layout[0]));
|
||||
};
|
||||
const layoutSelectInput = (selector) => {
|
||||
return useContextSelector(LayoutContextSelector, layout => selector(layout[0].input));
|
||||
};
|
||||
const layoutSelectOutput = (selector) => {
|
||||
return useContextSelector(LayoutContextSelector, layout => selector(layout[0].output));
|
||||
};
|
||||
const layoutDispatch = () => {
|
||||
return useContextSelector(LayoutContextSelector, layout => layout[1]);
|
||||
};
|
||||
const layoutSelect = (
|
||||
selector,
|
||||
) => useContextSelector(LayoutContextSelector, (layout) => selector(layout[0]));
|
||||
const layoutSelectInput = (
|
||||
selector,
|
||||
) => useContextSelector(LayoutContextSelector, (layout) => selector(layout[0].input));
|
||||
const layoutSelectOutput = (
|
||||
selector,
|
||||
) => useContextSelector(LayoutContextSelector, (layout) => selector(layout[0].output));
|
||||
const layoutDispatch = () => useContextSelector(LayoutContextSelector, (layout) => layout[1]);
|
||||
|
||||
export {
|
||||
LayoutContextProvider,
|
||||
@ -1465,4 +1533,4 @@ export {
|
||||
layoutSelectInput,
|
||||
layoutSelectOutput,
|
||||
layoutDispatch,
|
||||
}
|
||||
};
|
||||
|
@ -7,16 +7,11 @@ import PadContainer from '/imports/ui/components/pads/container';
|
||||
import Styled from './styles';
|
||||
import {
|
||||
PANELS, ACTIONS,
|
||||
LAYOUT_TYPE,
|
||||
PRESENTATION_AREA,
|
||||
} from '../layout/enums';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import Header from '/imports/ui/components/common/control-header/component';
|
||||
import NotesDropdown from '/imports/ui/components/notes/notes-dropdown/container';
|
||||
import { isPresentationEnabled } from '../../services/features';
|
||||
|
||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
||||
const DELAY_UNMOUNT_SHARED_NOTES = window.meetingClientSettings.public.app.delayForUnmountOfSharedNote;
|
||||
const intlMessages = defineMessages({
|
||||
hide: {
|
||||
@ -103,60 +98,6 @@ const Notes = ({
|
||||
}
|
||||
return () => clearTimeout(timoutRef);
|
||||
}, [isToSharedNotesBeShow, sidebarContent.sidebarContentPanel]);
|
||||
useEffect(() => {
|
||||
if (
|
||||
isOnMediaArea
|
||||
&& (sidebarContent.isOpen || !isPresentationEnabled())
|
||||
&& (sidebarContent.sidebarContentPanel === PANELS.SHARED_NOTES || !isPresentationEnabled())
|
||||
) {
|
||||
if (layoutType === LAYOUT_TYPE.VIDEO_FOCUS) {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.CHAT,
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_ID_CHAT_OPEN,
|
||||
value: PUBLIC_CHAT_ID,
|
||||
});
|
||||
} else {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.NONE,
|
||||
});
|
||||
}
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PILE_CONTENT_FOR_PRESENTATION_AREA,
|
||||
value: {
|
||||
content: PRESENTATION_AREA.PINNED_NOTES,
|
||||
open: true,
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PILE_CONTENT_FOR_PRESENTATION_AREA,
|
||||
value: {
|
||||
content: PRESENTATION_AREA.PINNED_NOTES,
|
||||
open: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
} if (shouldShowSharedNotesOnPresentationArea) {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PILE_CONTENT_FOR_PRESENTATION_AREA,
|
||||
value: {
|
||||
content: PRESENTATION_AREA.PINNED_NOTES,
|
||||
open: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderHeaderOnMedia = () => {
|
||||
return amIPresenter ? (
|
||||
|
@ -1,15 +1,15 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useMutation, useSubscription } from '@apollo/client';
|
||||
import { Session } from 'meteor/session';
|
||||
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
|
||||
import NotesService from '/imports/ui/components/notes/notes-graphql/service';
|
||||
import PadContainer from '/imports/ui/components/pads/container';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import Header from '/imports/ui/components/common/control-header/component';
|
||||
import NotesDropdown from './notes-dropdown/component';
|
||||
import { PANELS, ACTIONS, LAYOUT_TYPE } from '/imports/ui/components/layout/enums';
|
||||
import { isPresentationEnabled } from '/imports/ui/services/features';
|
||||
import {
|
||||
PANELS, ACTIONS,
|
||||
} from '/imports/ui/components/layout/enums';
|
||||
import { layoutSelectInput, layoutDispatch, layoutSelectOutput } from '/imports/ui/components/layout/context';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import useHasPermission from './hooks/useHasPermission';
|
||||
@ -22,9 +22,7 @@ import {
|
||||
isScreenBroadcasting,
|
||||
} from '/imports/ui/components/screenshare/service';
|
||||
|
||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||
const NOTES_CONFIG = window.meetingClientSettings.public.notes;
|
||||
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
||||
const DELAY_UNMOUNT_SHARED_NOTES = window.meetingClientSettings.public.app.delayForUnmountOfSharedNote;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -44,7 +42,6 @@ const intlMessages = defineMessages({
|
||||
|
||||
interface NotesContainerGraphqlProps {
|
||||
area: 'media' | undefined;
|
||||
layoutType: string;
|
||||
isToSharedNotesBeShow: boolean;
|
||||
}
|
||||
|
||||
@ -73,7 +70,6 @@ const NotesGraphql: React.FC<NotesGraphqlProps> = (props) => {
|
||||
layoutContextDispatch,
|
||||
isResizing,
|
||||
area,
|
||||
layoutType,
|
||||
sidebarContent,
|
||||
sharedNotesOutput,
|
||||
amIPresenter,
|
||||
@ -113,64 +109,6 @@ const NotesGraphql: React.FC<NotesGraphqlProps> = (props) => {
|
||||
}
|
||||
return () => clearTimeout(timoutRef);
|
||||
}, [isToSharedNotesBeShow, sidebarContent.sidebarContentPanel]);
|
||||
// eslint-disable-next-line consistent-return
|
||||
useEffect(() => {
|
||||
if (
|
||||
isOnMediaArea
|
||||
&& (sidebarContent.isOpen || !isPresentationEnabled())
|
||||
&& (sidebarContent.sidebarContentPanel === PANELS.SHARED_NOTES || !isPresentationEnabled())
|
||||
) {
|
||||
if (layoutType === LAYOUT_TYPE.VIDEO_FOCUS) {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.CHAT,
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_ID_CHAT_OPEN,
|
||||
value: PUBLIC_CHAT_ID,
|
||||
});
|
||||
} else {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.NONE,
|
||||
});
|
||||
}
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_NOTES_IS_PINNED,
|
||||
value: true,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_NOTES_IS_PINNED,
|
||||
value: false,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: Session.get('presentationLastState'),
|
||||
});
|
||||
};
|
||||
} if (shouldShowSharedNotesOnPresentationArea) {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_NOTES_IS_PINNED,
|
||||
value: true,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderHeaderOnMedia = () => {
|
||||
return amIPresenter ? (
|
||||
@ -228,7 +166,7 @@ const NotesGraphql: React.FC<NotesGraphqlProps> = (props) => {
|
||||
};
|
||||
|
||||
const NotesContainerGraphql: React.FC<NotesContainerGraphqlProps> = (props) => {
|
||||
const { area, layoutType, isToSharedNotesBeShow } = props;
|
||||
const { area, isToSharedNotesBeShow } = props;
|
||||
|
||||
const hasPermission = useHasPermission();
|
||||
const { data: pinnedPadData } = useSubscription<PinnedPadSubscriptionResponse>(PINNED_PAD_SUBSCRIPTION);
|
||||
@ -272,7 +210,6 @@ const NotesContainerGraphql: React.FC<NotesContainerGraphqlProps> = (props) => {
|
||||
amIPresenter={amIPresenter}
|
||||
shouldShowSharedNotesOnPresentationArea={shouldShowSharedNotesOnPresentationArea}
|
||||
isRTL={isRTL}
|
||||
layoutType={layoutType}
|
||||
isToSharedNotesBeShow={isToSharedNotesBeShow}
|
||||
handlePinSharedNotes={handlePinSharedNotes}
|
||||
/>
|
||||
|
@ -152,21 +152,16 @@ export default injectIntl(withTracker(({ intl }) => {
|
||||
}
|
||||
|
||||
const meetingId = Auth.meetingID;
|
||||
const breakouts = breakoutService.getBreakouts();
|
||||
|
||||
if (breakouts.length > 0) {
|
||||
const currentBreakout = breakouts.find((b) => b.breakoutId === meetingId);
|
||||
|
||||
if (currentBreakout) {
|
||||
data.message = (
|
||||
<MeetingRemainingTime />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Meeting = Meetings.findOne({ meetingId },
|
||||
{ fields: { isBreakout: 1, componentsFlags: 1 } });
|
||||
|
||||
if (Meeting.isBreakout) {
|
||||
data.message = (
|
||||
<MeetingRemainingTime />
|
||||
);
|
||||
}
|
||||
|
||||
if (Meeting) {
|
||||
const { isBreakout, componentsFlags } = Meeting;
|
||||
|
||||
|
@ -0,0 +1,89 @@
|
||||
import { useSubscription } from '@apollo/client';
|
||||
import React, { useEffect } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Notification, NotificationResponse, getNotificationsStream } from './queries';
|
||||
import useCurrentUser from '../../core/hooks/useCurrentUser';
|
||||
import { notify } from '../../services/notification';
|
||||
import {
|
||||
NotifyPublishedPoll,
|
||||
layoutUpdate,
|
||||
pendingGuestAlert,
|
||||
userJoinPushAlert,
|
||||
userLeavePushAlert,
|
||||
} from './service';
|
||||
|
||||
const Notifications: React.FC = () => {
|
||||
const [registeredAt, setRegisteredAt] = React.useState<string>(new Date().toISOString());
|
||||
const [greaterThanLastOne, setGreaterThanLastOne] = React.useState<number>(0);
|
||||
|
||||
const messageIndexRef = React.useRef<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[key: string]:(...arg: any[]) => void
|
||||
}>({
|
||||
'app.whiteboard.annotations.poll': NotifyPublishedPoll,
|
||||
'app.userList.guest.pendingGuestAlert': pendingGuestAlert,
|
||||
'app.notification.userJoinPushAlert': userJoinPushAlert,
|
||||
'app.notification.userLeavePushAlert': userLeavePushAlert,
|
||||
'app.layoutUpdate.label': layoutUpdate,
|
||||
});
|
||||
|
||||
const {
|
||||
data: currentUser,
|
||||
} = useCurrentUser((u) => ({
|
||||
registeredAt: u.registeredAt,
|
||||
presenter: u.presenter,
|
||||
isModerator: u.isModerator,
|
||||
}));
|
||||
|
||||
const {
|
||||
data: notificationsStream,
|
||||
} = useSubscription<NotificationResponse>(getNotificationsStream, {
|
||||
variables: { initialCursor: '2024-04-18' },
|
||||
});
|
||||
|
||||
const notifier = (notification: Notification) => {
|
||||
notify(
|
||||
<FormattedMessage
|
||||
id={notification.messageId}
|
||||
// @ts-ignore - JS code
|
||||
values={notification.messageValues}
|
||||
description={notification.messageDescription}
|
||||
/>,
|
||||
notification.notificationType,
|
||||
notification.icon,
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser && currentUser.registeredAt) {
|
||||
if (registeredAt !== currentUser.registeredAt) {
|
||||
setRegisteredAt(currentUser.registeredAt);
|
||||
}
|
||||
}
|
||||
}, [currentUser]);
|
||||
|
||||
useEffect(() => {
|
||||
if (notificationsStream && notificationsStream.notification_stream.length > 0) {
|
||||
notificationsStream.notification_stream.forEach((notification: Notification) => {
|
||||
const createdAt = new Date(notification.createdAt).getTime();
|
||||
if (createdAt > greaterThanLastOne) {
|
||||
setGreaterThanLastOne(createdAt);
|
||||
// Do something with the notification
|
||||
if (messageIndexRef.current[notification.messageId]) {
|
||||
messageIndexRef.current[notification.messageId](
|
||||
notification,
|
||||
notifier,
|
||||
currentUser?.isModerator,
|
||||
currentUser?.presenter,
|
||||
);
|
||||
} else {
|
||||
notifier(notification);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [notificationsStream]);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Notifications;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user