Merge pull request #13485 from antobinary/merge-24-dev
chore: Merge 2.4.x into 'develop'
This commit is contained in:
commit
53a160d616
@ -11,7 +11,7 @@ stages:
|
||||
|
||||
# define which docker image to use for builds
|
||||
default:
|
||||
image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2021-09-30
|
||||
image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2021-10-12
|
||||
|
||||
# This stage uses git to find out since when each package has been unmodified.
|
||||
# it then checks an API endpoint on the package server to find out for which of
|
||||
|
@ -419,7 +419,12 @@ public class ParamsProcessorUtil {
|
||||
}
|
||||
}
|
||||
|
||||
boolean learningDashboardEn = learningDashboardEnabled;
|
||||
boolean learningDashboardEn = false;
|
||||
int learningDashboardCleanupMins = 0;
|
||||
|
||||
// Learning Dashboard not allowed for Breakout Rooms
|
||||
if(!isBreakout) {
|
||||
learningDashboardEn = learningDashboardEnabled;
|
||||
if (!StringUtils.isEmpty(params.get(ApiParams.LEARNING_DASHBOARD_ENABLED))) {
|
||||
try {
|
||||
learningDashboardEn = Boolean.parseBoolean(params
|
||||
@ -431,7 +436,7 @@ public class ParamsProcessorUtil {
|
||||
}
|
||||
}
|
||||
|
||||
int learningDashboardCleanupMins = learningDashboardCleanupDelayInMinutes;
|
||||
learningDashboardCleanupMins = learningDashboardCleanupDelayInMinutes;
|
||||
if (!StringUtils.isEmpty(params.get(ApiParams.LEARNING_DASHBOARD_CLEANUP_DELAY_IN_MINUTES))) {
|
||||
try {
|
||||
learningDashboardCleanupMins = Integer.parseInt(params
|
||||
@ -442,6 +447,8 @@ public class ParamsProcessorUtil {
|
||||
internalMeetingId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Generate token to access Activity Report
|
||||
String learningDashboardAccessToken = "";
|
||||
|
@ -15,7 +15,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface GetChecksumConstraint {
|
||||
|
||||
String message() default "Invalid checksum: checksums do not match";
|
||||
String key() default "checksumError";
|
||||
String message() default "Checksums do not match";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface GuestPolicyConstraint {
|
||||
|
||||
String key() default "guestDeny";
|
||||
String message() default "User denied access for this session";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
@ -15,7 +15,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface IsBooleanConstraint {
|
||||
|
||||
String message() default "Validation error: value must be a boolean";
|
||||
String key() default "validationError";
|
||||
String message() default "Value must be a boolean";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
@ -15,7 +15,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface IsIntegralConstraint {
|
||||
|
||||
String message() default "Validation error: value must be an integral number";
|
||||
String key() default "validationError";
|
||||
String message() default "Value must be an integral number";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface MaxParticipantsConstraint {
|
||||
|
||||
String key() default "maxParticipantsReached";
|
||||
String message() default "The maximum number of participants for the meeting has been reached";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
@ -15,7 +15,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface MeetingEndedConstraint {
|
||||
|
||||
String message() default "You can not re-join a meeting that has already been forcibly ended";
|
||||
String key() default "meetingForciblyEnded";
|
||||
String message() default "You can not join a meeting that has already been forcibly ended";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
@ -16,7 +16,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface MeetingExistsConstraint {
|
||||
|
||||
String message() default "Invalid meeting ID: A meeting with that ID does not exist";
|
||||
String key() default "notFound";
|
||||
String message() default "A meeting with that ID does not exist";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
@ -2,16 +2,13 @@ package org.bigbluebutton.api.model.constraint;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@NotEmpty(message = "You must provide a meeting ID")
|
||||
@NotEmpty(key = "missingParamMeetingID", message = "You must provide a meeting ID")
|
||||
@Size(min = 2, max = 256, message = "Meeting ID must be between 2 and 256 characters")
|
||||
@Pattern(regexp = "^[^,]+$", message = "Meeting ID cannot contain ','")
|
||||
@Constraint(validatedBy = {})
|
||||
@ -19,6 +16,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface MeetingIDConstraint {
|
||||
|
||||
String key() default "validationError";
|
||||
String message() default "Invalid meeting ID";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
@ -2,23 +2,21 @@ package org.bigbluebutton.api.model.constraint;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@NotNull(message = "You must provide a meeting name")
|
||||
@NotEmpty(message = "You must provide a meeting name")
|
||||
@Size(min = 2, max = 256, message = "Meeting name must be between 2 and 256 characters")
|
||||
//@Pattern(regexp = "^[^,]+$", message = "Meeting name cannot contain ','")
|
||||
@Constraint(validatedBy = {})
|
||||
@Target(FIELD)
|
||||
@Retention(RUNTIME)
|
||||
public @interface MeetingNameConstraint {
|
||||
|
||||
String key() default "validationError";
|
||||
String message() default "Invalid meeting name";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
@ -15,7 +15,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface ModeratorPasswordConstraint {
|
||||
|
||||
String message() default "Invalid password: The supplied moderator password is incorrect";
|
||||
String key() default "invalidPassword";
|
||||
String message() default "The supplied moderator password is incorrect";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
package org.bigbluebutton.api.model.constraint;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.list.NotEmptyList;
|
||||
import org.bigbluebutton.api.model.validator.NotEmptyValidator;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Constraint(validatedBy = NotEmptyValidator.class)
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Repeatable(NotEmptyList.class)
|
||||
public @interface NotEmpty {
|
||||
|
||||
String key() default "emptyError";
|
||||
String message() default "Field must contain a value";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package org.bigbluebutton.api.model.constraint;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.list.NotNullList;
|
||||
import org.bigbluebutton.api.model.validator.NotNullValidator;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Repeatable(NotNullList.class)
|
||||
@Constraint(validatedBy = NotNullValidator.class)
|
||||
public @interface NotNull {
|
||||
|
||||
String key() default "nullError";
|
||||
String message() default "Value cannot be null";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
@ -2,8 +2,6 @@ package org.bigbluebutton.api.model.constraint;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@ -16,6 +14,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface PasswordConstraint {
|
||||
|
||||
String key() default "invalidPassword";
|
||||
String message() default "Invalid password";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
@ -0,0 +1,42 @@
|
||||
package org.bigbluebutton.api.model.constraint;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.list.PatternList;
|
||||
import org.bigbluebutton.api.model.validator.PatternValidator;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Repeatable(PatternList.class)
|
||||
@Constraint(validatedBy = PatternValidator.class)
|
||||
public @interface Pattern {
|
||||
|
||||
String regexp();
|
||||
Flag[] flags() default {};
|
||||
String key() default "validationError";
|
||||
String message() default "Value contains invalid characters";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
||||
enum Flag {
|
||||
UNIX_LINES(1),
|
||||
CASE_INSENSITIVE(2),
|
||||
COMMENTS(4),
|
||||
MULTILINE(8),
|
||||
DOTALL(32),
|
||||
UNICODE_CASE(64),
|
||||
CANON_EQ(128);
|
||||
|
||||
private final int value;
|
||||
|
||||
private Flag(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface PostChecksumConstraint {
|
||||
|
||||
String message() default "Invalid checksum: checksums do not match";
|
||||
String key() default "checksumError";
|
||||
String message() default "Checksums do not match";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
package org.bigbluebutton.api.model.constraint;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.list.SizeList;
|
||||
import org.bigbluebutton.api.model.validator.SizeValidator;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Repeatable(SizeList.class)
|
||||
@Constraint(validatedBy = SizeValidator.class)
|
||||
public @interface Size {
|
||||
|
||||
String key() default "sizeError";
|
||||
String message() default "Value does not conform to size restrictions";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
int min() default 0;
|
||||
int max() default 2147483647;
|
||||
}
|
@ -10,11 +10,13 @@ import java.lang.annotation.Target;
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@NotNull(key = "missingToken", message = "You must provide a session token")
|
||||
@Constraint(validatedBy = { UserSessionValidator.class })
|
||||
@Target(FIELD)
|
||||
@Retention(RUNTIME)
|
||||
public @interface UserSessionConstraint {
|
||||
|
||||
String key() default "missingSession";
|
||||
String message() default "Invalid session token";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
@ -0,0 +1,14 @@
|
||||
package org.bigbluebutton.api.model.constraint.list;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.NotEmpty;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface NotEmptyList {
|
||||
NotEmpty[] value();
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package org.bigbluebutton.api.model.constraint.list;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.NotNull;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface NotNullList {
|
||||
NotNull[] value();
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package org.bigbluebutton.api.model.constraint.list;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.Pattern;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface PatternList {
|
||||
Pattern[] value();
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package org.bigbluebutton.api.model.constraint.list;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.Size;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface SizeList {
|
||||
Size[] value();
|
||||
}
|
@ -31,8 +31,7 @@ public class CreateMeeting extends RequestWithChecksum<CreateMeeting.Params> {
|
||||
@MeetingIDConstraint
|
||||
private String meetingID;
|
||||
|
||||
//@NotEmpty(message = "You must provide a voice bridge")
|
||||
@IsIntegralConstraint(message = "Voice bridge must be a 5-digit integral value")
|
||||
@IsIntegralConstraint(message = "Voice bridge must be an integral value")
|
||||
private String voiceBridgeString;
|
||||
private Integer voiceBridge;
|
||||
|
||||
|
@ -2,12 +2,12 @@ package org.bigbluebutton.api.model.request;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.MeetingExistsConstraint;
|
||||
import org.bigbluebutton.api.model.constraint.MeetingIDConstraint;
|
||||
import org.bigbluebutton.api.model.constraint.NotEmpty;
|
||||
import org.bigbluebutton.api.model.constraint.PasswordConstraint;
|
||||
import org.bigbluebutton.api.model.shared.Checksum;
|
||||
import org.bigbluebutton.api.model.shared.ModeratorPassword;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.Map;
|
||||
|
||||
public class EndMeeting extends RequestWithChecksum<EndMeeting.Params> {
|
||||
|
@ -17,9 +17,7 @@ public class Enter implements Request<Enter.Params> {
|
||||
public String getValue() { return value; }
|
||||
}
|
||||
|
||||
@NotNull(message = "You must provide a session token")
|
||||
@UserSessionConstraint
|
||||
//@MaxParticipantsConstraint
|
||||
@GuestPolicyConstraint
|
||||
private String sessionToken;
|
||||
|
||||
|
@ -21,9 +21,7 @@ public class GuestWait implements Request<GuestWait.Params> {
|
||||
public String getValue() { return value; }
|
||||
}
|
||||
|
||||
@NotNull(message = "You must provide the session token")
|
||||
@UserSessionConstraint
|
||||
// @MaxParticipantsConstraint
|
||||
private String sessionToken;
|
||||
|
||||
@MeetingExistsConstraint
|
||||
|
@ -3,7 +3,6 @@ package org.bigbluebutton.api.model.request;
|
||||
import org.bigbluebutton.api.model.constraint.*;
|
||||
import org.bigbluebutton.api.model.shared.Checksum;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.Map;
|
||||
|
||||
public class JoinMeeting extends RequestWithChecksum<JoinMeeting.Params> {
|
||||
@ -25,17 +24,17 @@ public class JoinMeeting extends RequestWithChecksum<JoinMeeting.Params> {
|
||||
}
|
||||
|
||||
@MeetingIDConstraint
|
||||
@MeetingExistsConstraint
|
||||
@MeetingExistsConstraint(key = "invalidMeetingIdentifier")
|
||||
@MeetingEndedConstraint
|
||||
private String meetingID;
|
||||
|
||||
private String userID;
|
||||
|
||||
@NotEmpty(message = "You must provide your name")
|
||||
@NotEmpty(key = "missingParamFullName", message = "You must provide your name")
|
||||
private String fullName;
|
||||
|
||||
@PasswordConstraint
|
||||
@NotEmpty(message = "You must provide either the moderator or attendee password")
|
||||
@NotEmpty(key = "invalidPassword", message = "You must provide either the moderator or attendee password")
|
||||
private String password;
|
||||
|
||||
@IsBooleanConstraint(message = "Guest must be a boolean value (true or false)")
|
||||
|
@ -2,9 +2,9 @@ package org.bigbluebutton.api.model.request;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.MeetingExistsConstraint;
|
||||
import org.bigbluebutton.api.model.constraint.MeetingIDConstraint;
|
||||
import org.bigbluebutton.api.model.constraint.NotEmpty;
|
||||
import org.bigbluebutton.api.model.shared.Checksum;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.Map;
|
||||
|
||||
public class SetPollXML extends RequestWithChecksum<SetPollXML.Params> {
|
||||
@ -21,10 +21,10 @@ public class SetPollXML extends RequestWithChecksum<SetPollXML.Params> {
|
||||
}
|
||||
|
||||
@MeetingIDConstraint
|
||||
@MeetingExistsConstraint
|
||||
@MeetingExistsConstraint(key = "invalidMeetingIdentifier")
|
||||
private String meetingID;
|
||||
|
||||
@NotEmpty(message = "You must provide the poll")
|
||||
@NotEmpty(key = "configXMLError", message = "You did not pass a poll XML")
|
||||
private String pollXML;
|
||||
|
||||
public SetPollXML(Checksum checksum) {
|
||||
|
@ -17,7 +17,6 @@ public class SignOut implements Request<SignOut.Params> {
|
||||
public String getValue() { return value; }
|
||||
}
|
||||
|
||||
@NotNull(message = "You must provide a session token")
|
||||
@UserSessionConstraint
|
||||
private String sessionToken;
|
||||
|
||||
|
@ -3,7 +3,6 @@ package org.bigbluebutton.api.model.request;
|
||||
import org.bigbluebutton.api.model.constraint.*;
|
||||
import org.bigbluebutton.api.service.SessionService;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Map;
|
||||
|
||||
public class Stuns implements Request<Stuns.Params> {
|
||||
@ -18,7 +17,6 @@ public class Stuns implements Request<Stuns.Params> {
|
||||
public String getValue() { return value; }
|
||||
}
|
||||
|
||||
@NotNull(message = "You must provide a session token")
|
||||
@UserSessionConstraint
|
||||
private String sessionToken;
|
||||
|
||||
|
@ -1,15 +1,14 @@
|
||||
package org.bigbluebutton.api.model.shared;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.NotEmpty;
|
||||
import org.bigbluebutton.api.util.ParamsUtil;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public abstract class Checksum {
|
||||
|
||||
@NotEmpty(message = "You must provide the API call")
|
||||
@NotEmpty(message = "You must provide the API call", groups = ChecksumValidationGroup.class)
|
||||
protected String apiCall;
|
||||
|
||||
@NotEmpty(message = "You must provide the checksum")
|
||||
@NotEmpty(key = "checksumError", message = "You must provide the checksum", groups = ChecksumValidationGroup.class)
|
||||
protected String checksum;
|
||||
|
||||
protected String queryStringWithoutChecksum;
|
||||
|
@ -0,0 +1,4 @@
|
||||
package org.bigbluebutton.api.model.shared;
|
||||
|
||||
public interface ChecksumValidationGroup {
|
||||
}
|
@ -5,7 +5,7 @@ import org.bigbluebutton.api.util.ParamsUtil;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
@GetChecksumConstraint(message = "Checksums do not match")
|
||||
@GetChecksumConstraint(groups = ChecksumValidationGroup.class)
|
||||
public class GetChecksum extends Checksum {
|
||||
|
||||
@NotEmpty(message = "You must provide the query string")
|
||||
|
@ -1,15 +1,11 @@
|
||||
package org.bigbluebutton.api.model.shared;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.PostChecksumConstraint;
|
||||
import org.bigbluebutton.api.service.ValidationService;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
@PostChecksumConstraint(message = "Checksums do not match")
|
||||
@PostChecksumConstraint(groups = ChecksumValidationGroup.class)
|
||||
public class PostChecksum extends Checksum {
|
||||
|
||||
Map<String, String[]> params;
|
||||
@ -17,48 +13,7 @@ public class PostChecksum extends Checksum {
|
||||
public PostChecksum(String apiCall, String checksum, Map<String, String[]> params) {
|
||||
super(apiCall, checksum);
|
||||
this.params = params;
|
||||
buildQueryStringFromParamsMap();
|
||||
}
|
||||
|
||||
private void buildQueryStringFromParamsMap() {
|
||||
StringBuilder queryString = new StringBuilder();
|
||||
SortedSet<String> keys = new TreeSet<>(params.keySet());
|
||||
|
||||
boolean firstParam = true;
|
||||
for(String key: keys) {
|
||||
|
||||
if(key.equals("checksum")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(String value: params.get(key)) {
|
||||
if(firstParam) {
|
||||
firstParam = false;
|
||||
} else {
|
||||
queryString.append("&");
|
||||
}
|
||||
|
||||
queryString.append(key);
|
||||
queryString.append("=");
|
||||
|
||||
String encodedValue = encodeString(value);
|
||||
queryString.append(encodedValue);
|
||||
}
|
||||
}
|
||||
|
||||
queryStringWithoutChecksum = queryString.toString();
|
||||
}
|
||||
|
||||
private String encodeString(String stringToEncode) {
|
||||
String encodedResult;
|
||||
|
||||
try {
|
||||
encodedResult = URLEncoder.encode(stringToEncode, StandardCharsets.UTF_8.name());
|
||||
} catch(UnsupportedEncodingException ex) {
|
||||
encodedResult = stringToEncode;
|
||||
}
|
||||
|
||||
return encodedResult;
|
||||
queryStringWithoutChecksum = ValidationService.buildQueryStringFromParamsMap(params);
|
||||
}
|
||||
|
||||
public Map<String, String[]> getParams() { return params; }
|
||||
|
@ -0,0 +1,18 @@
|
||||
package org.bigbluebutton.api.model.validator;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.NotEmpty;
|
||||
|
||||
import javax.validation.ConstraintValidator;
|
||||
import javax.validation.ConstraintValidatorContext;
|
||||
|
||||
public class NotEmptyValidator implements ConstraintValidator<NotEmpty, String> {
|
||||
|
||||
@Override
|
||||
public void initialize(NotEmpty constraintAnnotation) {}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
|
||||
if(s == null) return true;
|
||||
return !s.isEmpty();
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package org.bigbluebutton.api.model.validator;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.NotNull;
|
||||
|
||||
import javax.validation.ConstraintValidator;
|
||||
import javax.validation.ConstraintValidatorContext;
|
||||
|
||||
public class NotNullValidator implements ConstraintValidator<NotNull, Object> {
|
||||
|
||||
@Override
|
||||
public void initialize(NotNull constraintAnnotation) {}
|
||||
|
||||
@Override
|
||||
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
|
||||
return !(o == null);
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package org.bigbluebutton.api.model.validator;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.Pattern;
|
||||
|
||||
import javax.validation.ConstraintValidator;
|
||||
import javax.validation.ConstraintValidatorContext;
|
||||
|
||||
public class PatternValidator implements ConstraintValidator<Pattern, String> {
|
||||
|
||||
String regexp;
|
||||
|
||||
@Override
|
||||
public void initialize(Pattern constraintAnnotation) {
|
||||
regexp = constraintAnnotation.regexp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
|
||||
if(s == null) return true;
|
||||
return java.util.regex.Pattern.matches(regexp, s);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package org.bigbluebutton.api.model.validator;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.Size;
|
||||
|
||||
import javax.validation.ConstraintValidator;
|
||||
import javax.validation.ConstraintValidatorContext;
|
||||
|
||||
public class SizeValidator implements ConstraintValidator<Size, String> {
|
||||
|
||||
private int min;
|
||||
private int max;
|
||||
|
||||
@Override
|
||||
public void initialize(Size constraintAnnotation) {
|
||||
min = constraintAnnotation.min();
|
||||
max = constraintAnnotation.max();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
|
||||
if(s == null) return true;
|
||||
return (s.length() >= min && s.length() <= max);
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package org.bigbluebutton.api.service;
|
||||
|
||||
import org.bigbluebutton.api.model.request.*;
|
||||
import org.bigbluebutton.api.model.shared.Checksum;
|
||||
import org.bigbluebutton.api.model.shared.ChecksumValidationGroup;
|
||||
import org.bigbluebutton.api.model.shared.GetChecksum;
|
||||
import org.bigbluebutton.api.model.shared.PostChecksum;
|
||||
import org.bigbluebutton.api.util.ParamsUtil;
|
||||
@ -12,6 +13,9 @@ import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validation;
|
||||
import javax.validation.Validator;
|
||||
import javax.validation.ValidatorFactory;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
public class ValidationService {
|
||||
@ -60,16 +64,16 @@ public class ValidationService {
|
||||
validator = validatorFactory.getValidator();
|
||||
}
|
||||
|
||||
public Set<String> validate(ApiCall apiCall, Map<String, String[]> params, String queryString) {
|
||||
public Map<String, String> validate(ApiCall apiCall, Map<String, String[]> params, String queryString) {
|
||||
log.info("Validating {} request with query string {}", apiCall.getName(), queryString);
|
||||
|
||||
params = sanitizeParams(params);
|
||||
|
||||
Request request = initializeRequest(apiCall, params, queryString);
|
||||
Set<String> violations = new HashSet<>();
|
||||
Map<String,String> violations = new HashMap<>();
|
||||
|
||||
if(request == null) {
|
||||
violations.add("validationError: Request not recognized");
|
||||
violations.put("validationError", "Request not recognized");
|
||||
} else {
|
||||
request.populateFromParamsMap(params);
|
||||
violations = performValidation(request);
|
||||
@ -87,6 +91,10 @@ public class ValidationService {
|
||||
checksumValue = params.get("checksum")[0];
|
||||
}
|
||||
|
||||
if(queryString == null || queryString.isEmpty()) {
|
||||
queryString = buildQueryStringFromParamsMap(params);
|
||||
}
|
||||
|
||||
switch(apiCall.requestType) {
|
||||
case GET:
|
||||
checksum = new GetChecksum(apiCall.getName(), checksumValue, queryString);
|
||||
@ -137,19 +145,44 @@ public class ValidationService {
|
||||
return request;
|
||||
}
|
||||
|
||||
private <R extends Request> Set<String> performValidation(R classToValidate) {
|
||||
Set<ConstraintViolation<R>> violations = validator.validate(classToValidate);
|
||||
Set<String> violationSet = new HashSet<>();
|
||||
private <R extends Request> Map<String, String> performValidation(R classToValidate) {
|
||||
Set<ConstraintViolation<R>> violations = validator.validate(classToValidate, ChecksumValidationGroup.class);
|
||||
|
||||
for(ConstraintViolation<R> violation: violations) {
|
||||
violationSet.add(violation.getMessage());
|
||||
if(violations.isEmpty()) {
|
||||
violations = validator.validate(classToValidate);
|
||||
}
|
||||
|
||||
if(violationSet.isEmpty()) {
|
||||
return buildViolationsMap(classToValidate, violations);
|
||||
}
|
||||
|
||||
private <R extends Request> Map<String, String> buildViolationsMap(R classToValidate, Set<ConstraintViolation<R>> violations) {
|
||||
Map<String, String> violationMap = new HashMap<>();
|
||||
|
||||
for(ConstraintViolation<R> violation: violations) {
|
||||
Map<String, Object> attributes = violation.getConstraintDescriptor().getAttributes();
|
||||
String key;
|
||||
String message;
|
||||
|
||||
if(attributes.containsKey("key")) {
|
||||
key = (String) attributes.get("key");
|
||||
} else {
|
||||
key = "validationError";
|
||||
}
|
||||
|
||||
if(attributes.containsKey("message")) {
|
||||
message = (String) attributes.get("message");
|
||||
} else {
|
||||
message = "An unknown validation error occurred";
|
||||
}
|
||||
|
||||
violationMap.put(key, message);
|
||||
}
|
||||
|
||||
if(violationMap.isEmpty()) {
|
||||
classToValidate.convertParamsFromString();
|
||||
}
|
||||
|
||||
return violationSet;
|
||||
return violationMap;
|
||||
}
|
||||
|
||||
private Map<String, String[]> sanitizeParams(Map<String, String[]> params) {
|
||||
@ -192,6 +225,47 @@ public class ValidationService {
|
||||
return mapString.toString();
|
||||
}
|
||||
|
||||
public static String buildQueryStringFromParamsMap(Map<String, String[]> params) {
|
||||
StringBuilder queryString = new StringBuilder();
|
||||
SortedSet<String> keys = new TreeSet<>(params.keySet());
|
||||
|
||||
boolean firstParam = true;
|
||||
for(String key: keys) {
|
||||
|
||||
if(key.equals("checksum")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(String value: params.get(key)) {
|
||||
if(firstParam) {
|
||||
firstParam = false;
|
||||
} else {
|
||||
queryString.append("&");
|
||||
}
|
||||
|
||||
queryString.append(key);
|
||||
queryString.append("=");
|
||||
|
||||
String encodedValue = encodeString(value);
|
||||
queryString.append(encodedValue);
|
||||
}
|
||||
}
|
||||
|
||||
return queryString.toString();
|
||||
}
|
||||
|
||||
private static String encodeString(String stringToEncode) {
|
||||
String encodedResult;
|
||||
|
||||
try {
|
||||
encodedResult = URLEncoder.encode(stringToEncode, StandardCharsets.UTF_8.name());
|
||||
} catch(UnsupportedEncodingException ex) {
|
||||
encodedResult = stringToEncode;
|
||||
}
|
||||
|
||||
return encodedResult;
|
||||
}
|
||||
|
||||
public void setSecuritySalt(String securitySalt) { this.securitySalt = securitySalt; }
|
||||
public String getSecuritySalt() { return securitySalt; }
|
||||
|
||||
|
@ -6,11 +6,30 @@ import { getUserEmojisSummary, emojiConfigs } from '../services/EmojiService';
|
||||
import UserAvatar from './UserAvatar';
|
||||
|
||||
class UsersTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
activityscoreOrder: 'desc',
|
||||
};
|
||||
}
|
||||
|
||||
toggleActivityScoreOrder() {
|
||||
const { activityscoreOrder } = this.state;
|
||||
|
||||
if (activityscoreOrder === 'asc') {
|
||||
this.setState({ activityscoreOrder: 'desc' });
|
||||
} else {
|
||||
this.setState({ activityscoreOrder: 'asc' });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
allUsers, totalOfActivityTime, totalOfPolls, tab,
|
||||
} = this.props;
|
||||
|
||||
const { activityscoreOrder } = this.state;
|
||||
|
||||
function getSumOfTime(eventsArr) {
|
||||
return eventsArr.reduce((prevVal, elem) => {
|
||||
if (elem.stoppedOn > 0) return prevVal + (elem.stoppedOn - elem.startedOn);
|
||||
@ -121,7 +140,10 @@ class UsersTable extends React.Component {
|
||||
<th className="px-4 py-3 text-center">
|
||||
<FormattedMessage id="app.learningDashboard.usersTable.colRaiseHands" defaultMessage="Raise Hand" />
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center">
|
||||
<th
|
||||
className={`px-4 py-3 text-center ${tab === 'overview_activityscore' ? 'cursor-pointer' : ''}`}
|
||||
onClick={() => { if (tab === 'overview_activityscore') this.toggleActivityScoreOrder(); }}
|
||||
>
|
||||
<FormattedMessage id="app.learningDashboard.usersTable.colActivityScore" defaultMessage="Activity Score" />
|
||||
{
|
||||
tab === 'overview_activityscore'
|
||||
@ -133,7 +155,12 @@ class UsersTable extends React.Component {
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 13l-5 5m0 0l-5-5m5 5V6" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d={activityscoreOrder === 'asc' ? 'M17 13l-5 5m0 0l-5-5m5 5V6' : 'M7 11l5-5m0 0l5 5m-5-5v12'}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
: null
|
||||
@ -148,8 +175,12 @@ class UsersTable extends React.Component {
|
||||
{ typeof allUsers === 'object' && Object.values(allUsers || {}).length > 0 ? (
|
||||
Object.values(allUsers || {})
|
||||
.sort((a, b) => {
|
||||
if (tab === 'overview_activityscore' && usersActivityScore[a.intId] < usersActivityScore[b.intId]) return 1;
|
||||
if (tab === 'overview_activityscore' && usersActivityScore[a.intId] > usersActivityScore[b.intId]) return -1;
|
||||
if (tab === 'overview_activityscore' && usersActivityScore[a.intId] < usersActivityScore[b.intId]) {
|
||||
return activityscoreOrder === 'desc' ? 1 : -1;
|
||||
}
|
||||
if (tab === 'overview_activityscore' && usersActivityScore[a.intId] > usersActivityScore[b.intId]) {
|
||||
return activityscoreOrder === 'desc' ? -1 : 1;
|
||||
}
|
||||
if (a.isModerator === false && b.isModerator === true) return 1;
|
||||
if (a.isModerator === true && b.isModerator === false) return -1;
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
||||
|
@ -6,8 +6,6 @@ ENV DEBIAN_FRONTEND noninteractive
|
||||
#Required to install Libreoffice 7
|
||||
RUN echo "deb http://deb.debian.org/debian buster-backports main" >> /etc/apt/sources.list
|
||||
|
||||
RUN apt update
|
||||
|
||||
RUN apt -y install locales-all fontconfig libxt6 libxrender1
|
||||
RUN apt install -y -t buster-backports libreoffice
|
||||
RUN apt update && apt -y install locales-all fontconfig libxt6 libxrender1
|
||||
RUN apt update && apt -y install -t buster-backports libreoffice
|
||||
|
||||
|
@ -1 +1 @@
|
||||
git clone --branch v2.5.2 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
git clone --branch v2.6.0-beta.5 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
|
@ -1 +1 @@
|
||||
BIGBLUEBUTTON_RELEASE=2.4-rc-1
|
||||
BIGBLUEBUTTON_RELEASE=2.4-rc-2
|
||||
|
@ -1178,7 +1178,7 @@ check_state() {
|
||||
echo "#"
|
||||
fi
|
||||
|
||||
if [ "$(echo "$HTML5_CONFIG" | yq r - public.media.sipjsHackViaWs)" == "false" ]; then
|
||||
if [ "$(echo "$HTML5_CONFIG" | yq r - public.media.sipjsHackViaWs)" != "true" ]; then
|
||||
if [ "$PROTOCOL" == "https" ]; then
|
||||
if ! cat $SIP_CONFIG | grep -v '#' | grep proxy_pass | head -n 1 | grep -q https; then
|
||||
echo "# Warning: You have this server defined for https, but in"
|
||||
@ -1412,7 +1412,6 @@ if [ $CHECK ]; then
|
||||
echo " kurento.ip: $(echo "$KURENTO_CONFIG" | yq r - kurento[0].ip)"
|
||||
echo " kurento.url: $(echo "$KURENTO_CONFIG" | yq r - kurento[0].url)"
|
||||
echo " kurento.sip_ip: $(echo "$KURENTO_CONFIG" | yq r - freeswitch.sip_ip)"
|
||||
echo " localIpAddress: $(echo "$KURENTO_CONFIG" | yq r - localIpAddress)"
|
||||
echo " recordScreenSharing: $(echo "$KURENTO_CONFIG" | yq r - recordScreenSharing)"
|
||||
echo " recordWebcams: $(echo "$KURENTO_CONFIG" | yq r - recordWebcams)"
|
||||
echo " codec_video_main: $(echo "$KURENTO_CONFIG" | yq r - conference-media-specs.codec_video_main)"
|
||||
|
@ -34,15 +34,23 @@ log_history=28
|
||||
find /var/bigbluebutton/ -maxdepth 1 -type d -name "*-*" -mtime +$history -exec rm -rf '{}' +
|
||||
|
||||
#
|
||||
# Delete streams in kurento older than N days
|
||||
# Delete streams from Kurento and mediasoup older than N days
|
||||
#
|
||||
kurento_dir=/var/kurento/
|
||||
mediasoup_dir=/var/mediasoup/
|
||||
|
||||
remove_stale_sfu_raw_files() {
|
||||
for app in recordings screenshare; do
|
||||
app_dir=/var/kurento/$app
|
||||
app_dir="${1}${app}"
|
||||
if [[ -d $app_dir ]]; then
|
||||
find $app_dir -name "*.mkv" -o -name "*.webm" -mtime +$history -delete
|
||||
find $app_dir -type d -empty -mtime +$history -exec rmdir '{}' +
|
||||
find "$app_dir" -name "*.mkv" -o -name "*.webm" -mtime +"$history" -delete
|
||||
find "$app_dir" -type d -empty -mtime +"$history" -exec rmdir '{}' +
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
remove_stale_sfu_raw_files "$kurento_dir"
|
||||
remove_stale_sfu_raw_files "$mediasoup_dir"
|
||||
|
||||
#
|
||||
# Delete FreeSWITCH wav/opus recordings older than N days
|
||||
|
@ -75,8 +75,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
textarea::-webkit-input-placeholder,
|
||||
input::-webkit-input-placeholder {
|
||||
::-webkit-input-placeholder {
|
||||
color: var(--palette-placeholder-text);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:-moz-placeholder, /* Firefox 4 to 18 */
|
||||
::-moz-placeholder { /* Firefox 19+ */
|
||||
color: var(--palette-placeholder-text);
|
||||
opacity: 1;
|
||||
}
|
||||
@ -89,6 +94,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
<script src="compatibility/adapter.js?v=VERSION" language="javascript"></script>
|
||||
<script src="compatibility/sip.js?v=VERSION" language="javascript"></script>
|
||||
<script src="compatibility/kurento-utils.js?v=VERSION" language="javascript"></script>
|
||||
<script src="compatibility/tflite-simd.js?v=VERSION" language="javascript"></script>
|
||||
<script src="compatibility/tflite.js?v=VERSION" language="javascript"></script>
|
||||
<!-- fonts -->
|
||||
<link rel="preload" href="fonts/BbbIcons/bbb-icons.woff?j1ntjp" as="font" crossorigin="anonymous"/>
|
||||
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-Light.woff" as="font" crossorigin="anonymous"/>
|
||||
|
@ -320,3 +320,12 @@
|
||||
.icon-bbb-device_list_selector:before {
|
||||
content: "\e95b";
|
||||
}
|
||||
.icon-bbb-presentation_off:before {
|
||||
content: "\e95c";
|
||||
}
|
||||
.icon-bbb-external-video:before {
|
||||
content: "\e95d";
|
||||
}
|
||||
.icon-bbb-external-video_off:before {
|
||||
content: "\e95e";
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ UPPER_DESTINATION_DIR=/usr/share/meteor
|
||||
DESTINATION_DIR=$UPPER_DESTINATION_DIR/bundle
|
||||
|
||||
SERVICE_FILES_DIR=/usr/lib/systemd/system
|
||||
LOCAL_PACKAGING_DIR=/home/bigbluebutton/dev/bigbluebutton/bigbluebutton-html5/dev_local_deployment
|
||||
LOCAL_PACKAGING_DIR=/home/bigbluebutton/dev/bigbluebutton/build/packages-template/bbb-html5
|
||||
|
||||
if [ ! -d "$LOCAL_PACKAGING_DIR" ]; then
|
||||
echo "Did not find LOCAL_PACKAGING_DIR=$LOCAL_PACKAGING_DIR"
|
||||
@ -59,13 +59,13 @@ NUMBER_OF_FRONTEND_NODEJS_PROCESSES=2
|
||||
HERE
|
||||
|
||||
echo "writing $DESTINATION_DIR/systemd_start.sh"
|
||||
sudo cp $LOCAL_PACKAGING_DIR/systemd_start.sh "$DESTINATION_DIR"/systemd_start.sh
|
||||
sudo cp $LOCAL_PACKAGING_DIR/bionic/systemd_start.sh "$DESTINATION_DIR"/systemd_start.sh
|
||||
|
||||
echo "writing $DESTINATION_DIR/systemd_start_frontend.sh"
|
||||
sudo cp $LOCAL_PACKAGING_DIR/systemd_start_frontend.sh "$DESTINATION_DIR"/systemd_start_frontend.sh
|
||||
sudo cp $LOCAL_PACKAGING_DIR/bionic/systemd_start_frontend.sh "$DESTINATION_DIR"/systemd_start_frontend.sh
|
||||
|
||||
echo "writing $DESTINATION_DIR/workers-start.sh"
|
||||
sudo cp $LOCAL_PACKAGING_DIR/workers-start.sh "$DESTINATION_DIR"/workers-start.sh
|
||||
sudo cp $LOCAL_PACKAGING_DIR/bionic/workers-start.sh "$DESTINATION_DIR"/workers-start.sh
|
||||
|
||||
sudo chown -R meteor:meteor "$UPPER_DESTINATION_DIR"/
|
||||
sudo chmod +x "$DESTINATION_DIR"/mongod_start_pre.sh
|
||||
@ -76,10 +76,10 @@ sudo chmod +x "$DESTINATION_DIR"/workers-start.sh
|
||||
|
||||
|
||||
echo "writing $SERVICE_FILES_DIR/bbb-html5-frontend@.service"
|
||||
sudo cp $LOCAL_PACKAGING_DIR/bbb-html5-frontend@.service "$SERVICE_FILES_DIR"/bbb-html5-frontend@.service
|
||||
sudo cp $LOCAL_PACKAGING_DIR/bionic/bbb-html5-frontend@.service "$SERVICE_FILES_DIR"/bbb-html5-frontend@.service
|
||||
|
||||
echo "writing $SERVICE_FILES_DIR/bbb-html5-backend@.service"
|
||||
sudo cp $LOCAL_PACKAGING_DIR/bbb-html5-backend@.service "$SERVICE_FILES_DIR"/bbb-html5-backend@.service
|
||||
sudo cp $LOCAL_PACKAGING_DIR/bionic/bbb-html5-backend@.service "$SERVICE_FILES_DIR"/bbb-html5-backend@.service
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
|
@ -1,9 +0,0 @@
|
||||
Last change on Feb 16, 2021
|
||||
|
||||
This directory contains files needed for the correct deployment of bigbluebutton-html5 **on a development environment**.
|
||||
|
||||
They are very similar, or even identical to the files used for `bbb-html5` packaging, however, the main difference is that this set of files may be unintentionally **out of date**.
|
||||
|
||||
The script `deploy_to_usr_share.sh` was written to allow developers to be able to wipe out the `/usr/share/meteor` directory where `bbb-html5` package is installed, and at the same time build their local code and deploy it so it replaces the default `bbb-html5`. The script has been indispensible during the work on https://github.com/bigbluebutton/bigbluebutton/pull/11317 where multiple NodeJS processes were to run simultaneously but using different configuration.
|
||||
|
||||
|
@ -1,24 +0,0 @@
|
||||
[Unit]
|
||||
Description=BigBlueButton HTML5 service, backend instance %i
|
||||
Requires=bbb-html5.service
|
||||
Before=bbb-html5.service
|
||||
BindsTo=bbb-html5.service
|
||||
|
||||
[Service]
|
||||
PermissionsStartOnly=true
|
||||
#Type=simple
|
||||
Type=idle
|
||||
EnvironmentFile=/usr/share/meteor/bundle/bbb-html5-with-roles.conf
|
||||
ExecStart=/usr/share/meteor/bundle/systemd_start.sh %i $BACKEND_NODEJS_ROLE
|
||||
WorkingDirectory=/usr/share/meteor/bundle
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
TimeoutStartSec=10
|
||||
RestartSec=10
|
||||
User=meteor
|
||||
Group=meteor
|
||||
CPUSchedulingPolicy=fifo
|
||||
Nice=19
|
||||
|
||||
[Install]
|
||||
WantedBy=bbb-html5.service
|
@ -1,24 +0,0 @@
|
||||
[Unit]
|
||||
Description=BigBlueButton HTML5 service, frontend instance %i
|
||||
Requires=bbb-html5.service
|
||||
Before=bbb-html5.service
|
||||
BindsTo=bbb-html5.service
|
||||
|
||||
[Service]
|
||||
PermissionsStartOnly=true
|
||||
#Type=simple
|
||||
Type=idle
|
||||
EnvironmentFile=/usr/share/meteor/bundle/bbb-html5-with-roles.conf
|
||||
ExecStart=/usr/share/meteor/bundle/systemd_start_frontend.sh %i
|
||||
WorkingDirectory=/usr/share/meteor/bundle
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
TimeoutStartSec=10
|
||||
RestartSec=10
|
||||
User=meteor
|
||||
Group=meteor
|
||||
CPUSchedulingPolicy=fifo
|
||||
Nice=19
|
||||
|
||||
[Install]
|
||||
WantedBy=bbb-html5.service
|
@ -1,32 +0,0 @@
|
||||
# mongod.conf
|
||||
|
||||
# for documentation of all options, see:
|
||||
# http://docs.mongodb.org/manual/reference/configuration-options/
|
||||
|
||||
storage:
|
||||
dbPath: /mnt/mongo-ramdisk
|
||||
journal:
|
||||
enabled: true
|
||||
wiredTiger:
|
||||
engineConfig:
|
||||
cacheSizeGB: 0
|
||||
journalCompressor: none
|
||||
directoryForIndexes: true
|
||||
collectionConfig:
|
||||
blockCompressor: none
|
||||
indexConfig:
|
||||
prefixCompression: false
|
||||
|
||||
systemLog:
|
||||
destination: file
|
||||
logAppend: true
|
||||
path: /var/log/mongodb/mongod.log
|
||||
|
||||
net:
|
||||
port: 27017
|
||||
bindIp: 127.0.1.1
|
||||
|
||||
|
||||
replication:
|
||||
replSetName: rs0
|
||||
|
@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
rm -rf /mnt/mongo-ramdisk/*
|
||||
mkdir -p /mnt/mongo-ramdisk
|
||||
if /bin/findmnt | grep -q "/mnt/mongo-ramdisk"; then
|
||||
umount /mnt/mongo-ramdisk/
|
||||
fi
|
||||
if [ ! -f /.dockerenv ]; then
|
||||
mount -t tmpfs -o size=512m tmpfs /mnt/mongo-ramdisk
|
||||
fi
|
||||
|
||||
chown -R mongodb:mongodb /mnt/mongo-ramdisk
|
||||
|
||||
|
@ -1,51 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
#Allow to run outside of directory
|
||||
cd $(dirname $0)
|
||||
|
||||
echo "Starting mongoDB"
|
||||
|
||||
#wait for mongo startup
|
||||
MONGO_OK=0
|
||||
|
||||
while [ "$MONGO_OK" = "0" ]; do
|
||||
MONGO_OK=$(netstat -lan | grep 127.0.1.1 | grep 27017 &> /dev/null && echo 1 || echo 0)
|
||||
sleep 1;
|
||||
done;
|
||||
|
||||
echo "Mongo started";
|
||||
|
||||
echo "Initializing replicaset"
|
||||
mongo 127.0.1.1 --eval 'rs.initiate({ _id: "rs0", members: [ {_id: 0, host: "127.0.1.1"} ]})'
|
||||
|
||||
|
||||
echo "Waiting to become a master"
|
||||
IS_MASTER="XX"
|
||||
while [ "$IS_MASTER" \!= "true" ]; do
|
||||
IS_MASTER=$(mongo mongodb://127.0.1.1:27017/ --eval 'db.isMaster().ismaster' | tail -n 1)
|
||||
sleep 0.5;
|
||||
done;
|
||||
|
||||
echo "I'm the master!"
|
||||
|
||||
if [ -z $1 ]
|
||||
then
|
||||
INSTANCE_ID=1
|
||||
else
|
||||
INSTANCE_ID=$1
|
||||
fi
|
||||
|
||||
PORT=$(echo "3999+$INSTANCE_ID" | bc)
|
||||
|
||||
echo "instanceId = $INSTANCE_ID and port = $PORT and role is backend (in backend file)"
|
||||
|
||||
export INSTANCE_ID=$INSTANCE_ID
|
||||
export BBB_HTML5_ROLE=backend
|
||||
export ROOT_URL=http://127.0.0.1/html5client
|
||||
export MONGO_OPLOG_URL=mongodb://127.0.1.1/local
|
||||
export MONGO_URL=mongodb://127.0.1.1/meteor
|
||||
export NODE_ENV=production
|
||||
export NODE_VERSION=node-v14.17.6-linux-x64
|
||||
export SERVER_WEBSOCKET_COMPRESSION=0
|
||||
export BIND_IP=127.0.0.1
|
||||
PORT=$PORT /usr/share/$NODE_VERSION/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=$INSTANCE_ID
|
@ -1,51 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
#Allow to run outside of directory
|
||||
cd $(dirname $0)
|
||||
|
||||
echo "Starting mongoDB"
|
||||
|
||||
#wait for mongo startup
|
||||
MONGO_OK=0
|
||||
|
||||
while [ "$MONGO_OK" = "0" ]; do
|
||||
MONGO_OK=$(netstat -lan | grep 127.0.1.1 | grep 27017 &> /dev/null && echo 1 || echo 0)
|
||||
sleep 1;
|
||||
done;
|
||||
|
||||
echo "Mongo started";
|
||||
|
||||
echo "Initializing replicaset"
|
||||
mongo 127.0.1.1 --eval 'rs.initiate({ _id: "rs0", members: [ {_id: 0, host: "127.0.1.1"} ]})'
|
||||
|
||||
|
||||
echo "Waiting to become a master"
|
||||
IS_MASTER="XX"
|
||||
while [ "$IS_MASTER" \!= "true" ]; do
|
||||
IS_MASTER=$(mongo mongodb://127.0.1.1:27017/ --eval 'db.isMaster().ismaster' | tail -n 1)
|
||||
sleep 0.5;
|
||||
done;
|
||||
|
||||
echo "I'm the master!"
|
||||
|
||||
if [ -z $1 ]
|
||||
then
|
||||
INSTANCE_ID=1
|
||||
else
|
||||
INSTANCE_ID=$1
|
||||
fi
|
||||
|
||||
PORT=$(echo "4099+$INSTANCE_ID" | bc)
|
||||
|
||||
echo "instanceId = $INSTANCE_ID and port = $PORT and role is frontend (in frontend file)"
|
||||
|
||||
export INSTANCE_ID=$INSTANCE_ID
|
||||
export BBB_HTML5_ROLE=frontend
|
||||
export ROOT_URL=http://127.0.0.1/html5client
|
||||
export MONGO_OPLOG_URL=mongodb://127.0.1.1/local
|
||||
export MONGO_URL=mongodb://127.0.1.1/meteor
|
||||
export NODE_ENV=production
|
||||
export NODE_VERSION=node-v14.17.6-linux-x64
|
||||
export SERVER_WEBSOCKET_COMPRESSION=0
|
||||
export BIND_IP=127.0.0.1
|
||||
PORT=$PORT /usr/share/$NODE_VERSION/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js
|
@ -1,33 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Start parallel nodejs processes for bbb-html5. Number varies on restrictions file bbb-html5-with-roles.conf
|
||||
|
||||
source /usr/share/meteor/bundle/bbb-html5-with-roles.conf
|
||||
|
||||
if [ -f /etc/bigbluebutton/bbb-html5-with-roles.conf ]; then
|
||||
source /etc/bigbluebutton/bbb-html5-with-roles.conf
|
||||
fi
|
||||
|
||||
MIN_NUMBER_OF_BACKEND_PROCESSES=1
|
||||
MAX_NUMBER_OF_BACKEND_PROCESSES=4
|
||||
|
||||
MIN_NUMBER_OF_FRONTEND_PROCESSES=0 # 0 means each nodejs process handles both front and backend roles
|
||||
MAX_NUMBER_OF_FRONTEND_PROCESSES=8
|
||||
|
||||
|
||||
# Start backend nodejs processes
|
||||
if ((NUMBER_OF_BACKEND_NODEJS_PROCESSES >= MIN_NUMBER_OF_BACKEND_PROCESSES && NUMBER_OF_BACKEND_NODEJS_PROCESSES <= MAX_NUMBER_OF_BACKEND_PROCESSES)); then
|
||||
for ((i = 1 ; i <= NUMBER_OF_BACKEND_NODEJS_PROCESSES ; i++)); do
|
||||
systemctl start bbb-html5-backend@$i
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Start frontend nodejs processes
|
||||
if ((NUMBER_OF_FRONTEND_NODEJS_PROCESSES >= MIN_NUMBER_OF_FRONTEND_PROCESSES && NUMBER_OF_FRONTEND_NODEJS_PROCESSES <= MAX_NUMBER_OF_FRONTEND_PROCESSES)); then
|
||||
if ((NUMBER_OF_FRONTEND_NODEJS_PROCESSES == 0)); then
|
||||
echo 'Need to modify /etc/bigbluebutton/nginx/bbb-html5.nginx to ensure backend IPs are used'
|
||||
fi
|
||||
for ((i = 1 ; i <= NUMBER_OF_FRONTEND_NODEJS_PROCESSES ; i++)); do
|
||||
systemctl start bbb-html5-frontend@$i
|
||||
done
|
||||
fi
|
@ -11,6 +11,7 @@ import getFromMeetingSettings from '/imports/ui/services/meeting-settings';
|
||||
|
||||
const SFU_URL = Meteor.settings.public.kurento.wsUrl;
|
||||
const DEFAULT_LISTENONLY_MEDIA_SERVER = Meteor.settings.public.kurento.listenOnlyMediaServer;
|
||||
const SIGNAL_CANDIDATES = Meteor.settings.public.kurento.signalCandidates;
|
||||
const MEDIA = Meteor.settings.public.media;
|
||||
const MEDIA_TAG = MEDIA.mediaTag.replace(/#/g, '');
|
||||
const GLOBAL_AUDIO_PREFIX = 'GLOBAL_AUDIO_';
|
||||
@ -265,6 +266,7 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
|
||||
iceServers,
|
||||
offering: OFFERING,
|
||||
mediaServer: getMediaServerAdapter(),
|
||||
signalCandidates: SIGNAL_CANDIDATES,
|
||||
};
|
||||
|
||||
this.broker = new ListenOnlyBroker(
|
||||
|
@ -98,6 +98,7 @@ class SIPSession {
|
||||
this._hangupFlag = false;
|
||||
this._reconnecting = false;
|
||||
this._currentSessionState = null;
|
||||
this._ignoreCallState = false;
|
||||
}
|
||||
|
||||
get inputStream() {
|
||||
@ -226,6 +227,24 @@ class SIPSession {
|
||||
this._outputDeviceId = deviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* This _ignoreCallState flag is set to true when we want to ignore SIP's
|
||||
* call state retrieved directly from FreeSWITCH ESL, when doing some checks
|
||||
* (for example , when checking if call stopped).
|
||||
* We need to ignore this , for example, when moderator is in
|
||||
* breakout audio transfer ("Join Audio" button in breakout panel): in this
|
||||
* case , we will monitor moderator's lifecycle in audio conference by
|
||||
* using the SIP state taken from SIP.js only (ignoring the ESL's call state).
|
||||
* @param {boolean} value true to ignore call state, false otherwise.
|
||||
*/
|
||||
set ignoreCallState(value) {
|
||||
this._ignoreCallState = value;
|
||||
}
|
||||
|
||||
get ignoreCallState() {
|
||||
return this._ignoreCallState;
|
||||
}
|
||||
|
||||
joinAudio({
|
||||
isListenOnly,
|
||||
extension,
|
||||
@ -236,6 +255,8 @@ class SIPSession {
|
||||
return new Promise((resolve, reject) => {
|
||||
const callExtension = extension ? `${extension}${this.userData.voiceBridge}` : this.userData.voiceBridge;
|
||||
|
||||
this.ignoreCallState = false;
|
||||
|
||||
const callback = (message) => {
|
||||
// There will sometimes we erroneous errors put out like timeouts and improper shutdowns,
|
||||
// but only the first error ever matters
|
||||
@ -1068,7 +1089,9 @@ class SIPSession {
|
||||
};
|
||||
|
||||
const checkIfCallStopped = (message) => {
|
||||
if (fsReady || !sessionTerminated) return null;
|
||||
if ((!this.ignoreCallState && fsReady) || !sessionTerminated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!message && !!this.userRequestedHangup) {
|
||||
return this.callback({
|
||||
@ -1350,6 +1373,20 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
return this.activeSession ? this.activeSession.inputStream : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for SIPSession's ignoreCallState flag
|
||||
* @param {boolean} value
|
||||
*/
|
||||
set ignoreCallState(value) {
|
||||
if (this.activeSession) {
|
||||
this.activeSession.ignoreCallState = value;
|
||||
}
|
||||
}
|
||||
|
||||
get ignoreCallState() {
|
||||
return this.activeSession ? this.activeSession.ignoreCallState : false;
|
||||
}
|
||||
|
||||
joinAudio({ isListenOnly, extension, validIceCandidates }, managerCallback) {
|
||||
const hasFallbackDomain = typeof IPV4_FALLBACK_DOMAIN === 'string' && IPV4_FALLBACK_DOMAIN !== '';
|
||||
|
||||
|
@ -15,26 +15,12 @@ export default function handleBreakoutJoinURL({ body }) {
|
||||
breakoutId,
|
||||
};
|
||||
|
||||
// only keep each users' last invitation
|
||||
const newUsers = [];
|
||||
|
||||
const currentBreakout = Breakouts.findOne({ breakoutId }, { fields: { users: 1 } });
|
||||
|
||||
currentBreakout.users.forEach((item) => {
|
||||
if (item.userId !== userId) {
|
||||
newUsers.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
newUsers.push({
|
||||
userId,
|
||||
redirectToHtml5JoinURL,
|
||||
insertedTime: new Date().getTime(),
|
||||
});
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
users: newUsers,
|
||||
[`url_${userId}`]: {
|
||||
redirectToHtml5JoinURL,
|
||||
insertedTime: new Date().getTime(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -23,7 +23,6 @@ export default function handleBreakoutRoomStarted({ body }, meetingId) {
|
||||
const modifier = {
|
||||
$set: Object.assign(
|
||||
{
|
||||
users: [],
|
||||
joinedUsers: [],
|
||||
},
|
||||
{ timeRemaining: DEFAULT_TIME_REMAINING },
|
||||
|
@ -50,7 +50,7 @@ function breakouts() {
|
||||
},
|
||||
{
|
||||
parentMeetingId: meetingId,
|
||||
'users.userId': userId,
|
||||
[`url_${userId}`]: { $exists: true },
|
||||
},
|
||||
{
|
||||
breakoutId: meetingId,
|
||||
@ -60,12 +60,7 @@ function breakouts() {
|
||||
|
||||
const fields = {
|
||||
fields: {
|
||||
users: {
|
||||
$elemMatch: {
|
||||
// do not allow users to obtain 'redirectToHtml5JoinURL' for others
|
||||
userId,
|
||||
},
|
||||
},
|
||||
[`url_${userId}`]: 1,
|
||||
breakoutId: 1,
|
||||
externalId: 1,
|
||||
freeJoin: 1,
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import takeOwnership from '/imports/api/captions/server/methods/takeOwnership';
|
||||
import appendText from '/imports/api/captions/server/methods/appendText';
|
||||
import getPadId from '/imports/api/captions/server/methods/getPadId';
|
||||
|
||||
Meteor.methods({
|
||||
takeOwnership,
|
||||
appendText,
|
||||
getPadId,
|
||||
});
|
||||
|
@ -2,21 +2,22 @@ import axios from 'axios';
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import Captions from '/imports/api/captions';
|
||||
import { CAPTIONS_TOKEN } from '/imports/api/captions/server/helpers';
|
||||
import { appendTextURL } from '/imports/api/common/server/etherpad';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
|
||||
export default function appendText(text, locale) {
|
||||
try {
|
||||
const { meetingId } = extractCredentials(this.userId);
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(text, String);
|
||||
check(locale, String);
|
||||
|
||||
const captions = Captions.findOne({
|
||||
meetingId,
|
||||
padId: { $regex: `${CAPTIONS_TOKEN}${locale}$` },
|
||||
locale,
|
||||
ownerId: requesterUserId,
|
||||
});
|
||||
|
||||
if (!captions) {
|
||||
|
@ -35,7 +35,7 @@ export default function createCaptions(meetingId, instanceId) {
|
||||
const locales = response.data;
|
||||
locales.forEach((locale) => {
|
||||
const padId = withInstaceId(instanceId, generatePadId(meetingId, locale.locale));
|
||||
addCaption(meetingId, padId, locale);
|
||||
addCaption(meetingId, padId, locale.locale, locale.name);
|
||||
padIds.push(padId);
|
||||
});
|
||||
addCaptionsPads(meetingId, padIds);
|
||||
|
@ -26,20 +26,22 @@ export default function editCaptions(padId, data) {
|
||||
meetingId,
|
||||
ownerId,
|
||||
locale,
|
||||
name,
|
||||
length,
|
||||
} = pad;
|
||||
|
||||
check(meetingId, String);
|
||||
check(ownerId, String);
|
||||
check(locale, { locale: String, name: String });
|
||||
check(locale, String);
|
||||
check(name, String);
|
||||
check(length, Number);
|
||||
|
||||
const index = getIndex(data, length);
|
||||
|
||||
const payload = {
|
||||
startIndex: index,
|
||||
localeCode: locale.locale,
|
||||
locale: locale.name,
|
||||
localeCode: locale,
|
||||
locale: name,
|
||||
endIndex: index,
|
||||
text: data,
|
||||
};
|
||||
|
@ -0,0 +1,49 @@
|
||||
import Captions from '/imports/api/captions';
|
||||
import Users from '/imports/api/users';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
|
||||
const hasPadAccess = (meetingId, userId) => {
|
||||
const user = Users.findOne(
|
||||
{ meetingId, userId },
|
||||
{ fields: { role: 1 }},
|
||||
);
|
||||
|
||||
if (!user) return false;
|
||||
|
||||
if (user.role === ROLE_MODERATOR) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export default function getPadId(locale) {
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
const caption = Captions.findOne(
|
||||
{ meetingId, locale },
|
||||
{
|
||||
fields: {
|
||||
padId: 1,
|
||||
ownerId: 1,
|
||||
readOnlyPadId: 1,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (caption) {
|
||||
if (hasPadAccess(meetingId, requesterUserId)) {
|
||||
if (requesterUserId === caption.ownerId) return caption.padId;
|
||||
|
||||
return caption.readOnlyPadId;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (err) {
|
||||
return null;;
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ import { check } from 'meteor/check';
|
||||
import Captions from '/imports/api/captions';
|
||||
import updateOwnerId from '/imports/api/captions/server/modifiers/updateOwnerId';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import { CAPTIONS_TOKEN } from '/imports/api/captions/server/helpers';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function takeOwnership(locale) {
|
||||
@ -10,12 +9,10 @@ export default function takeOwnership(locale) {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(locale, String);
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
const pad = Captions.findOne({ meetingId, padId: { $regex: `${CAPTIONS_TOKEN}${locale}$` } });
|
||||
|
||||
if (pad) {
|
||||
updateOwnerId(meetingId, requesterUserId, pad.padId);
|
||||
}
|
||||
updateOwnerId(meetingId, requesterUserId, locale);
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method takeOwnership ${err.stack}`);
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import Logger from '/imports/startup/server/logger';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
|
||||
export default function updateOwner(meetingId, userId, padId) { // TODO
|
||||
export default function updateOwner(meetingId, userId, locale) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'UpdateCaptionOwnerPubMsg';
|
||||
@ -12,23 +12,19 @@ export default function updateOwner(meetingId, userId, padId) { // TODO
|
||||
try {
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
check(padId, String);
|
||||
check(locale, String);
|
||||
|
||||
const pad = Captions.findOne({ meetingId, padId });
|
||||
const pad = Captions.findOne({ meetingId, locale });
|
||||
|
||||
if (!pad) {
|
||||
Logger.error(`Editing captions owner: ${padId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { locale } = pad;
|
||||
|
||||
check(locale, { locale: String, name: String });
|
||||
|
||||
const payload = {
|
||||
ownerId: userId,
|
||||
locale: locale.name,
|
||||
localeCode: locale.locale,
|
||||
locale: pad.name,
|
||||
localeCode: pad.locale,
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, userId, payload);
|
||||
|
@ -2,13 +2,11 @@ import { check } from 'meteor/check';
|
||||
import Captions from '/imports/api/captions';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function addCaption(meetingId, padId, locale) {
|
||||
export default function addCaption(meetingId, padId, locale, name) {
|
||||
check(meetingId, String);
|
||||
check(padId, String);
|
||||
check(locale, {
|
||||
locale: String,
|
||||
name: String,
|
||||
});
|
||||
check(locale, String);
|
||||
check(name, String);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
@ -19,6 +17,7 @@ export default function addCaption(meetingId, padId, locale) {
|
||||
meetingId,
|
||||
padId,
|
||||
locale,
|
||||
name,
|
||||
ownerId: '',
|
||||
readOnlyPadId: '',
|
||||
data: '',
|
||||
@ -30,9 +29,9 @@ export default function addCaption(meetingId, padId, locale) {
|
||||
const { insertedId, numberAffected } = Captions.upsert(selector, modifier);
|
||||
|
||||
if (insertedId) {
|
||||
Logger.verbose('Captions: added locale', { locale: locale.locale, meetingId });
|
||||
Logger.verbose('Captions: added locale', { locale, meetingId });
|
||||
} else if (numberAffected) {
|
||||
Logger.verbose('Captions: upserted locale', { locale: locale.locale, meetingId });
|
||||
Logger.verbose('Captions: upserted locale', { locale, meetingId });
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Adding caption to collection: ${err}`);
|
||||
|
@ -3,14 +3,14 @@ import Logger from '/imports/startup/server/logger';
|
||||
import updateOwner from '/imports/api/captions/server/methods/updateOwner';
|
||||
import { check } from 'meteor/check';
|
||||
|
||||
export default function updateOwnerId(meetingId, userId, padId) {
|
||||
export default function updateOwnerId(meetingId, userId, locale) {
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
check(padId, String);
|
||||
check(locale, String);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
padId,
|
||||
locale,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
@ -23,8 +23,8 @@ export default function updateOwnerId(meetingId, userId, padId) {
|
||||
const numberAffected = Captions.update(selector, modifier, { multi: true });
|
||||
|
||||
if (numberAffected) {
|
||||
updateOwner(meetingId, userId, padId);
|
||||
Logger.verbose('Captions: updated caption', { padId, ownerId: userId });
|
||||
updateOwner(meetingId, userId, locale);
|
||||
Logger.verbose('Captions: updated caption', { locale, ownerId: userId });
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error('Captions: error while updating pad', { err });
|
||||
|
@ -14,7 +14,14 @@ function captions() {
|
||||
const { meetingId, userId } = tokenValidation;
|
||||
Logger.debug('Publishing Captions', { meetingId, requestedBy: userId });
|
||||
|
||||
return Captions.find({ meetingId });
|
||||
const options = {
|
||||
fields: {
|
||||
padId: 0,
|
||||
readOnlyPadId: 0,
|
||||
},
|
||||
};
|
||||
|
||||
return Captions.find({ meetingId }, options);
|
||||
}
|
||||
|
||||
function publish(...args) {
|
||||
|
@ -1,2 +1,3 @@
|
||||
import './publishers';
|
||||
import './methods';
|
||||
import './eventHandlers';
|
||||
|
6
bigbluebutton-html5/imports/api/note/server/methods.js
Normal file
6
bigbluebutton-html5/imports/api/note/server/methods.js
Normal file
@ -0,0 +1,6 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import getNoteId from './methods/getNoteId';
|
||||
|
||||
Meteor.methods({
|
||||
getNoteId,
|
||||
});
|
@ -0,0 +1,65 @@
|
||||
import Note from '/imports/api/note';
|
||||
import Users from '/imports/api/users';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
|
||||
const ROLE_VIEWER = Meteor.settings.public.user.role_viewer;
|
||||
|
||||
const hasNoteAccess = (meetingId, userId) => {
|
||||
const user = Users.findOne(
|
||||
{ meetingId, userId },
|
||||
{
|
||||
fields: {
|
||||
role: 1,
|
||||
locked: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!user) return false;
|
||||
|
||||
if (user.role === ROLE_VIEWER && user.locked) {
|
||||
const meeting = Meetings.findOne(
|
||||
{ meetingId },
|
||||
{ fields: { 'lockSettingsProps.disableNote': 1 } }
|
||||
);
|
||||
|
||||
if (!meeting) return false;
|
||||
|
||||
const { lockSettingsProps } = meeting;
|
||||
if (lockSettingsProps) {
|
||||
if (lockSettingsProps.disableNote) return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export default function getNoteId() {
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
const note = Note.findOne(
|
||||
{ meetingId },
|
||||
{
|
||||
fields: {
|
||||
noteId: 1,
|
||||
readOnlyNoteId: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (note) {
|
||||
if (hasNoteAccess(meetingId, requesterUserId)) {
|
||||
return note.noteId;
|
||||
}
|
||||
return note.readOnlyNoteId;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -15,7 +15,14 @@ function note() {
|
||||
|
||||
Logger.info(`Publishing Note for ${meetingId} ${userId}`);
|
||||
|
||||
return Note.find({ meetingId });
|
||||
const options = {
|
||||
fields: {
|
||||
noteId: 0,
|
||||
readOnlyNoteId: 0,
|
||||
},
|
||||
};
|
||||
|
||||
return Note.find({ meetingId }, options);
|
||||
}
|
||||
|
||||
function publish(...args) {
|
||||
|
@ -8,6 +8,7 @@ import { SCREENSHARING_ERRORS } from './errors';
|
||||
const SFU_CONFIG = Meteor.settings.public.kurento;
|
||||
const SFU_URL = SFU_CONFIG.wsUrl;
|
||||
const OFFERING = SFU_CONFIG.screenshare.subscriberOffering;
|
||||
const SIGNAL_CANDIDATES = Meteor.settings.public.kurento.signalCandidates;
|
||||
|
||||
const BRIDGE_NAME = 'kurento'
|
||||
const SCREENSHARE_VIDEO_TAG = 'screenshareVideo';
|
||||
@ -225,6 +226,7 @@ export default class KurentoScreenshareBridge {
|
||||
hasAudio,
|
||||
offering: OFFERING,
|
||||
mediaServer: BridgeService.getMediaServerAdapter(),
|
||||
signalCandidates: SIGNAL_CANDIDATES,
|
||||
};
|
||||
|
||||
this.broker = new ScreenshareBroker(
|
||||
@ -284,6 +286,7 @@ export default class KurentoScreenshareBridge {
|
||||
bitrate: BridgeService.BASE_BITRATE,
|
||||
offering: true,
|
||||
mediaServer: BridgeService.getMediaServerAdapter(),
|
||||
signalCandidates: SIGNAL_CANDIDATES,
|
||||
};
|
||||
|
||||
this.broker = new ScreenshareBroker(
|
||||
|
@ -29,8 +29,6 @@ export default function handlePresenterAssigned({ body }, meetingId) {
|
||||
presenter: true,
|
||||
};
|
||||
|
||||
const prevPresenter = Users.findOne(selector);
|
||||
|
||||
const defaultPodSelector = {
|
||||
meetingId,
|
||||
podId: 'DEFAULT_PRESENTATION_POD',
|
||||
@ -44,28 +42,23 @@ export default function handlePresenterAssigned({ body }, meetingId) {
|
||||
presenterId,
|
||||
};
|
||||
|
||||
// no previous presenters
|
||||
// The below code is responsible for set Meeting presenter to be default pod presenter as well.
|
||||
// It's been handled here because right now akka-apps don't handle all cases scenarios.
|
||||
if (!prevPresenter) {
|
||||
const { currentPresenterId } = currentDefaultPod;
|
||||
|
||||
if (currentPresenterId === '') {
|
||||
return setPresenterInPodReqMsg(setPresenterPayload);
|
||||
}
|
||||
|
||||
const oldPresenter = Users.findOne({ meetingId, userId: currentPresenterId });
|
||||
|
||||
if (oldPresenter?.userId !== currentPresenterId) {
|
||||
return setPresenterInPodReqMsg(setPresenterPayload);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentDefaultPod && currentDefaultPod.currentPresenterId !== presenterId) {
|
||||
setPresenterInPodReqMsg(setPresenterPayload);
|
||||
}
|
||||
const prevPresenter = Users.findOne(selector);
|
||||
|
||||
if (prevPresenter) {
|
||||
changePresenter(false, prevPresenter.userId, meetingId, assignedBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* In the cases where the first moderator joins the meeting or
|
||||
* the current presenter left the meeting, akka-apps doesn't assign the new presenter
|
||||
* to the default presentation pod. This step is done manually here.
|
||||
*/
|
||||
|
||||
if (currentDefaultPod.currentPresenterId !== presenterId) {
|
||||
const presenterToBeAssigned = Users.findOne({ userId: presenterId });
|
||||
|
||||
if (!presenterToBeAssigned) setPresenterPayload.presenterId = '';
|
||||
|
||||
setPresenterInPodReqMsg(setPresenterPayload);
|
||||
}
|
||||
}
|
||||
|
@ -124,6 +124,7 @@ class ActionsDropdown extends PureComponent {
|
||||
stopExternalVideoShare,
|
||||
mountModal,
|
||||
layoutContextDispatch,
|
||||
hidePresentation,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -138,7 +139,7 @@ class ActionsDropdown extends PureComponent {
|
||||
|
||||
const actions = [];
|
||||
|
||||
if (amIPresenter) {
|
||||
if (amIPresenter && !hidePresentation) {
|
||||
actions.push({
|
||||
icon: "presentation",
|
||||
dataTest: "uploadPresentation",
|
||||
@ -183,7 +184,7 @@ class ActionsDropdown extends PureComponent {
|
||||
|
||||
if (amIPresenter && allowExternalVideo) {
|
||||
actions.push({
|
||||
icon: "video",
|
||||
icon: !isSharingVideo ? "external-video" : "external-video_off",
|
||||
label: !isSharingVideo ? intl.formatMessage(intlMessages.startExternalVideoLabel)
|
||||
: intl.formatMessage(intlMessages.stopExternalVideoLabel),
|
||||
key: "external-video",
|
||||
|
@ -5,6 +5,7 @@ import PresentationUploaderService from '/imports/ui/components/presentation/pre
|
||||
import PresentationPodService from '/imports/ui/components/presentation-pod/service';
|
||||
import ActionsDropdown from './component';
|
||||
import { layoutSelectInput, layoutDispatch } from '../../layout/context';
|
||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
|
||||
const ActionsDropdownContainer = (props) => {
|
||||
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
|
||||
@ -22,6 +23,8 @@ const ActionsDropdownContainer = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const LAYOUT_CONFIG = Meteor.settings.public.layout;
|
||||
|
||||
export default withTracker(() => {
|
||||
const presentations = Presentations.find({ 'conversion.done': true }).fetch();
|
||||
return ({
|
||||
@ -29,5 +32,6 @@ export default withTracker(() => {
|
||||
isDropdownOpen: Session.get('dropdownOpen'),
|
||||
setPresentation: PresentationUploaderService.setPresentation,
|
||||
podIds: PresentationPodService.getPresentationPodIds(),
|
||||
hidePresentation: getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation),
|
||||
});
|
||||
})(ActionsDropdownContainer);
|
||||
|
@ -21,6 +21,7 @@ class ActionsBar extends PureComponent {
|
||||
handleTakePresenter,
|
||||
intl,
|
||||
isSharingVideo,
|
||||
hasScreenshare,
|
||||
stopExternalVideoShare,
|
||||
isCaptionsAvailable,
|
||||
isMeteorConnected,
|
||||
@ -80,15 +81,14 @@ class ActionsBar extends PureComponent {
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
{isLayoutSwapped && !isPresentationDisabled
|
||||
? (
|
||||
<PresentationOptionsContainer
|
||||
isLayoutSwapped={isLayoutSwapped}
|
||||
toggleSwapLayout={toggleSwapLayout}
|
||||
layoutContextDispatch={layoutContextDispatch}
|
||||
isThereCurrentPresentation={isThereCurrentPresentation}
|
||||
hasPresentation={isThereCurrentPresentation}
|
||||
hasExternalVideo={isSharingVideo}
|
||||
hasScreenshare={hasScreenshare}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
{isRaiseHandButtonEnabled
|
||||
? (
|
||||
<Button
|
||||
|
@ -13,10 +13,10 @@ import UserListService from '/imports/ui/components/user-list/service';
|
||||
import ExternalVideoService from '/imports/ui/components/external-video-player/service';
|
||||
import CaptionsService from '/imports/ui/components/captions/service';
|
||||
import { layoutSelectOutput, layoutDispatch } from '../layout/context';
|
||||
import { isVideoBroadcasting } from '/imports/ui/components/screenshare/service';
|
||||
|
||||
import MediaService, {
|
||||
getSwapLayout,
|
||||
shouldEnableSwapLayout,
|
||||
} from '../media/service';
|
||||
|
||||
const ActionsBarContainer = (props) => {
|
||||
@ -51,12 +51,13 @@ export default withTracker(() => ({
|
||||
amIModerator: Service.amIModerator(),
|
||||
stopExternalVideoShare: ExternalVideoService.stopWatching,
|
||||
enableVideo: getFromUserSettings('bbb_enable_video', Meteor.settings.public.kurento.enableVideo),
|
||||
isLayoutSwapped: getSwapLayout() && shouldEnableSwapLayout(),
|
||||
isLayoutSwapped: getSwapLayout(),
|
||||
toggleSwapLayout: MediaService.toggleSwapLayout,
|
||||
handleTakePresenter: Service.takePresenterRole,
|
||||
currentSlidHasContent: PresentationService.currentSlidHasContent(),
|
||||
parseCurrentSlideContent: PresentationService.parseCurrentSlideContent,
|
||||
isSharingVideo: Service.isSharingVideo(),
|
||||
hasScreenshare: isVideoBroadcasting(),
|
||||
isCaptionsAvailable: CaptionsService.isCaptionsAvailable(),
|
||||
isMeteorConnected: Meteor.status().connected,
|
||||
isPollingEnabled: POLLING_ENABLED,
|
||||
|
@ -227,7 +227,8 @@ class BreakoutRoom extends PureComponent {
|
||||
componentDidUpdate(prevProps, prevstate) {
|
||||
if (this.listOfUsers) {
|
||||
for (let i = 0; i < this.listOfUsers.children.length; i += 1) {
|
||||
const roomList = this.listOfUsers.children[i].getElementsByTagName('div')[0];
|
||||
const roomWrapperChildren = this.listOfUsers.children[i].getElementsByTagName('div');
|
||||
const roomList = roomWrapperChildren[roomWrapperChildren.length > 1 ? 1 : 0];
|
||||
roomList.addEventListener('keydown', this.handleMoveEvent, true);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,9 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import MediaService from '/imports/ui/components/media/service';
|
||||
import cx from 'classnames';
|
||||
import { styles } from '../styles';
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
@ -11,6 +14,14 @@ const propTypes = {
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
minimizePresentationLabel: {
|
||||
id: 'app.actionsBar.actionsDropdown.minimizePresentationLabel',
|
||||
description: '',
|
||||
},
|
||||
minimizePresentationDesc: {
|
||||
id: 'app.actionsBar.actionsDropdown.restorePresentationDesc',
|
||||
description: '',
|
||||
},
|
||||
restorePresentationLabel: {
|
||||
id: 'app.actionsBar.actionsDropdown.restorePresentationLabel',
|
||||
description: 'Restore Presentation option label',
|
||||
@ -23,24 +34,41 @@ const intlMessages = defineMessages({
|
||||
|
||||
const PresentationOptionsContainer = ({
|
||||
intl,
|
||||
isLayoutSwapped,
|
||||
toggleSwapLayout,
|
||||
isThereCurrentPresentation,
|
||||
layoutContextDispatch,
|
||||
}) => (
|
||||
hasPresentation,
|
||||
hasExternalVideo,
|
||||
hasScreenshare,
|
||||
}) => {
|
||||
let buttonType = 'presentation';
|
||||
if (hasExternalVideo) {
|
||||
// hack until we have an external-video icon
|
||||
buttonType = 'external-video';
|
||||
} else if (hasScreenshare) {
|
||||
buttonType = 'desktop';
|
||||
}
|
||||
|
||||
const isThereCurrentPresentation = hasExternalVideo || hasScreenshare || hasPresentation;
|
||||
return (
|
||||
<Button
|
||||
icon="presentation"
|
||||
data-test="restorePresentationButton"
|
||||
label={intl.formatMessage(intlMessages.restorePresentationLabel)}
|
||||
description={intl.formatMessage(intlMessages.restorePresentationDesc)}
|
||||
color="primary"
|
||||
className={cx(styles.button, !isLayoutSwapped || styles.btn)}
|
||||
icon={`${buttonType}${isLayoutSwapped ? '_off' : ''}`}
|
||||
label={intl.formatMessage(isLayoutSwapped ? intlMessages.restorePresentationLabel : intlMessages.minimizePresentationLabel)}
|
||||
aria-label={intl.formatMessage(isLayoutSwapped ? intlMessages.restorePresentationLabel : intlMessages.minimizePresentationLabel)}
|
||||
aria-describedby={intl.formatMessage(isLayoutSwapped ? intlMessages.restorePresentationDesc : intlMessages.minimizePresentationDesc)}
|
||||
description={intl.formatMessage(isLayoutSwapped ? intlMessages.restorePresentationDesc : intlMessages.minimizePresentationDesc)}
|
||||
color={!isLayoutSwapped ? "primary" : "default"}
|
||||
hideLabel
|
||||
circle
|
||||
size="lg"
|
||||
onClick={() => toggleSwapLayout(layoutContextDispatch)}
|
||||
id="restore-presentation"
|
||||
ghost={isLayoutSwapped}
|
||||
disabled={!isThereCurrentPresentation}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
PresentationOptionsContainer.propTypes = propTypes;
|
||||
export default injectIntl(PresentationOptionsContainer);
|
||||
|
@ -149,6 +149,7 @@ class App extends Component {
|
||||
meetingLayout,
|
||||
settingsLayout,
|
||||
isRTL,
|
||||
hidePresentation,
|
||||
} = this.props;
|
||||
const { browserName } = browserInfo;
|
||||
const { osName } = deviceInfo;
|
||||
@ -160,6 +161,11 @@ class App extends Component {
|
||||
value: isRTL,
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: !hidePresentation,
|
||||
});
|
||||
|
||||
MediaService.setSwapLayout(layoutContextDispatch);
|
||||
Modal.setAppElement('#app');
|
||||
|
||||
|
@ -167,6 +167,7 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
|
||||
}).fetch();
|
||||
|
||||
const AppSettings = Settings.application;
|
||||
const { selectedLayout } = AppSettings;
|
||||
const { viewScreenshare } = Settings.dataSaving;
|
||||
const shouldShowExternalVideo = MediaService.shouldShowExternalVideo();
|
||||
const shouldShowScreenshare = MediaService.shouldShowScreenshare()
|
||||
@ -177,6 +178,8 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
|
||||
customStyleUrl = CUSTOM_STYLE_URL;
|
||||
}
|
||||
|
||||
const LAYOUT_CONFIG = Meteor.settings.public.layout;
|
||||
|
||||
return {
|
||||
captions: CaptionsService.isCaptionsActive() ? <CaptionsContainer /> : null,
|
||||
fontSize: getFontSize(),
|
||||
@ -195,8 +198,8 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
|
||||
currentUserId: currentUser?.userId,
|
||||
isPresenter: currentUser?.presenter,
|
||||
meetingLayout: layout,
|
||||
settingsLayout: AppSettings.selectedLayout,
|
||||
pushLayoutToEveryone: AppSettings.pushLayoutToEveryone,
|
||||
settingsLayout: selectedLayout?.replace('Push', ''),
|
||||
pushLayoutToEveryone: selectedLayout?.includes('Push'),
|
||||
audioAlertEnabled: AppSettings.chatAudioAlerts,
|
||||
pushAlertEnabled: AppSettings.chatPushAlerts,
|
||||
shouldShowScreenshare,
|
||||
@ -207,6 +210,7 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
|
||||
'bbb_force_restore_presentation_on_new_events',
|
||||
Meteor.settings.public.presentation.restoreOnUpdate,
|
||||
),
|
||||
hidePresentation: getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation),
|
||||
};
|
||||
})(AppContainer)));
|
||||
|
||||
|
@ -192,7 +192,7 @@ class BreakoutJoinConfirmation extends Component {
|
||||
))
|
||||
}
|
||||
</select>
|
||||
{ waiting ? <span>{intl.formatMessage(intlMessages.generatingURL)}</span> : null}
|
||||
{ waiting ? <span data-test="labelGeneratingURL">{intl.formatMessage(intlMessages.generatingURL)}</span> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -15,9 +15,9 @@ const BreakoutJoinConfirmationContrainer = (props) => (
|
||||
|
||||
const getURL = (breakoutId) => {
|
||||
const currentUserId = Auth.userID;
|
||||
const getBreakout = Breakouts.findOne({ breakoutId }, { fields: { users: 1 } });
|
||||
const user = getBreakout ? getBreakout.users?.find((u) => u.userId === currentUserId) : '';
|
||||
if (user) return user.redirectToHtml5JoinURL;
|
||||
const breakout = Breakouts.findOne({ breakoutId }, { fields: { [`url_${currentUserId}`]: 1 } });
|
||||
const breakoutUrlData = (breakout && breakout[`url_${currentUserId}`]) ? breakout[`url_${currentUserId}`] : null;
|
||||
if (breakoutUrlData) return breakoutUrlData.redirectToHtml5JoinURL;
|
||||
return '';
|
||||
};
|
||||
|
||||
|
@ -43,18 +43,14 @@ const intlMessages = defineMessages({
|
||||
id: 'app.createBreakoutRoom.returnAudio',
|
||||
description: 'label for option to return audio',
|
||||
},
|
||||
generateURL: {
|
||||
id: 'app.createBreakoutRoom.generateURL',
|
||||
askToJoin: {
|
||||
id: 'app.createBreakoutRoom.askToJoin',
|
||||
description: 'label for generate breakout room url',
|
||||
},
|
||||
generatingURL: {
|
||||
id: 'app.createBreakoutRoom.generatingURL',
|
||||
description: 'label for generating breakout room url',
|
||||
},
|
||||
generatedURL: {
|
||||
id: 'app.createBreakoutRoom.generatedURL',
|
||||
description: 'label for generated breakout room url',
|
||||
},
|
||||
endAllBreakouts: {
|
||||
id: 'app.createBreakoutRoom.endAllBreakouts',
|
||||
description: 'Button label to end all breakout rooms',
|
||||
@ -125,7 +121,7 @@ class BreakoutRoom extends PureComponent {
|
||||
|
||||
componentDidUpdate() {
|
||||
const {
|
||||
breakoutRoomUser,
|
||||
getBreakoutRoomUrl,
|
||||
setBreakoutAudioTransferStatus,
|
||||
isMicrophoneUser,
|
||||
isReconnecting,
|
||||
@ -144,10 +140,11 @@ class BreakoutRoom extends PureComponent {
|
||||
}
|
||||
|
||||
if (waiting && !generated) {
|
||||
const breakoutUser = breakoutRoomUser(requestedBreakoutId);
|
||||
const breakoutUrlData = getBreakoutRoomUrl(requestedBreakoutId);
|
||||
|
||||
if (!breakoutUser) return false;
|
||||
if (breakoutUser.redirectToHtml5JoinURL !== '') {
|
||||
if (!breakoutUrlData) return false;
|
||||
if (breakoutUrlData.redirectToHtml5JoinURL !== '') {
|
||||
window.open(breakoutUrlData.redirectToHtml5JoinURL, '_blank');
|
||||
_.delay(() => this.setState({ generated: true, waiting: false }), 1000);
|
||||
}
|
||||
}
|
||||
@ -164,10 +161,10 @@ class BreakoutRoom extends PureComponent {
|
||||
|
||||
getBreakoutURL(breakoutId) {
|
||||
Session.set('lastBreakoutOpened', breakoutId);
|
||||
const { requestJoinURL, breakoutRoomUser } = this.props;
|
||||
const { requestJoinURL, getBreakoutRoomUrl } = this.props;
|
||||
const { waiting } = this.state;
|
||||
const hasUser = breakoutRoomUser(breakoutId);
|
||||
if (!hasUser && !waiting) {
|
||||
const breakoutRoomUrlData = getBreakoutRoomUrl(breakoutId);
|
||||
if (!breakoutRoomUrlData && !waiting) {
|
||||
this.setState(
|
||||
{
|
||||
waiting: true,
|
||||
@ -178,28 +175,28 @@ class BreakoutRoom extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
if (hasUser) {
|
||||
window.open(hasUser.redirectToHtml5JoinURL, '_blank');
|
||||
if (breakoutRoomUrlData) {
|
||||
window.open(breakoutRoomUrlData.redirectToHtml5JoinURL, '_blank');
|
||||
this.setState({ waiting: false, generated: false });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getBreakoutLabel(breakoutId) {
|
||||
const { intl, breakoutRoomUser } = this.props;
|
||||
const { intl, getBreakoutRoomUrl } = this.props;
|
||||
const { requestedBreakoutId, generated } = this.state;
|
||||
|
||||
const hasUser = breakoutRoomUser(breakoutId);
|
||||
const breakoutRoomUrlData = getBreakoutRoomUrl(breakoutId);
|
||||
|
||||
if (generated && requestedBreakoutId === breakoutId) {
|
||||
return intl.formatMessage(intlMessages.generatedURL);
|
||||
}
|
||||
|
||||
if (hasUser) {
|
||||
return intl.formatMessage(intlMessages.breakoutJoin);
|
||||
}
|
||||
|
||||
return intl.formatMessage(intlMessages.generateURL);
|
||||
if (breakoutRoomUrlData) {
|
||||
return intl.formatMessage(intlMessages.breakoutJoin);
|
||||
}
|
||||
|
||||
return intl.formatMessage(intlMessages.askToJoin);
|
||||
}
|
||||
|
||||
clearJoinedAudioOnly() {
|
||||
@ -490,7 +487,7 @@ class BreakoutRoom extends PureComponent {
|
||||
messageDuration={intlMessages.breakoutDuration}
|
||||
breakoutRoom={breakoutRooms[0]}
|
||||
/>
|
||||
{!visibleExtendTimeForm
|
||||
{amIModerator && !visibleExtendTimeForm
|
||||
? (
|
||||
<Button
|
||||
onClick={this.showExtendTimeForm}
|
||||
@ -539,6 +536,7 @@ class BreakoutRoom extends PureComponent {
|
||||
size="lg"
|
||||
label={intl.formatMessage(intlMessages.endAllBreakouts)}
|
||||
className={styles.endButton}
|
||||
data-test="endBreakoutRoomsButton"
|
||||
onClick={() => {
|
||||
this.closePanel();
|
||||
endAllBreakouts();
|
||||
|
@ -19,7 +19,7 @@ export default withTracker((props) => {
|
||||
extendBreakoutsTime,
|
||||
isExtendTimeHigherThanMeetingRemaining,
|
||||
findBreakouts,
|
||||
breakoutRoomUser,
|
||||
getBreakoutRoomUrl,
|
||||
transferUserToMeeting,
|
||||
transferToBreakout,
|
||||
meetingId,
|
||||
@ -43,7 +43,7 @@ export default withTracker((props) => {
|
||||
requestJoinURL,
|
||||
extendBreakoutsTime,
|
||||
isExtendTimeHigherThanMeetingRemaining,
|
||||
breakoutRoomUser,
|
||||
getBreakoutRoomUrl,
|
||||
transferUserToMeeting,
|
||||
transferToBreakout,
|
||||
isMicrophoneUser,
|
||||
|
@ -9,10 +9,9 @@ const BREAKOUT_MODAL_DELAY = 200;
|
||||
|
||||
const propTypes = {
|
||||
mountModal: PropTypes.func.isRequired,
|
||||
currentBreakoutUser: PropTypes.shape({
|
||||
currentBreakoutUrlData: PropTypes.shape({
|
||||
insertedTime: PropTypes.number.isRequired,
|
||||
}),
|
||||
getBreakoutByUser: PropTypes.func.isRequired,
|
||||
breakoutUserIsIn: PropTypes.shape({
|
||||
sequence: PropTypes.number.isRequired,
|
||||
}),
|
||||
@ -22,7 +21,7 @@ const propTypes = {
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
currentBreakoutUser: undefined,
|
||||
currentBreakoutUrlData: undefined,
|
||||
breakoutUserIsIn: undefined,
|
||||
breakouts: [],
|
||||
};
|
||||
@ -55,8 +54,8 @@ class BreakoutRoomInvitation extends Component {
|
||||
checkBreakouts(oldProps) {
|
||||
const {
|
||||
breakouts,
|
||||
currentBreakoutUser,
|
||||
getBreakoutByUser,
|
||||
currentBreakoutUrlData,
|
||||
getBreakoutByUrlData,
|
||||
breakoutUserIsIn,
|
||||
} = this.props;
|
||||
|
||||
@ -67,19 +66,19 @@ class BreakoutRoomInvitation extends Component {
|
||||
const hasBreakouts = breakouts.length > 0;
|
||||
|
||||
if (hasBreakouts && !breakoutUserIsIn && BreakoutService.checkInviteModerators()) {
|
||||
// Have to check for freeJoin breakouts first because currentBreakoutUser will
|
||||
// Have to check for freeJoin breakouts first because currentBreakoutUrlData will
|
||||
// populate after a room has been joined
|
||||
const breakoutRoom = getBreakoutByUser(currentBreakoutUser);
|
||||
const freeJoinBreakout = breakouts.find(breakout => breakout.freeJoin);
|
||||
const breakoutRoom = getBreakoutByUrlData(currentBreakoutUrlData);
|
||||
const freeJoinBreakout = breakouts.find((breakout) => breakout.freeJoin);
|
||||
if (freeJoinBreakout) {
|
||||
if (!didSendBreakoutInvite) {
|
||||
this.inviteUserToBreakout(breakoutRoom || freeJoinBreakout);
|
||||
this.setState({ didSendBreakoutInvite: true });
|
||||
}
|
||||
} else if (currentBreakoutUser) {
|
||||
const currentInsertedTime = currentBreakoutUser.insertedTime;
|
||||
const oldCurrentUser = oldProps.currentBreakoutUser || {};
|
||||
const oldInsertedTime = oldCurrentUser.insertedTime;
|
||||
} else if (currentBreakoutUrlData) {
|
||||
const currentInsertedTime = currentBreakoutUrlData.insertedTime;
|
||||
const oldCurrentUrlData = oldProps.currentBreakoutUrlData || {};
|
||||
const oldInsertedTime = oldCurrentUrlData.insertedTime;
|
||||
if (currentInsertedTime !== oldInsertedTime) {
|
||||
const breakoutId = Session.get('lastBreakoutOpened');
|
||||
if (breakoutRoom.breakoutId !== breakoutId) {
|
||||
|
@ -15,7 +15,7 @@ const BreakoutRoomInvitationContainer = ({ isMeetingBreakout, ...props }) => {
|
||||
export default withTracker(() => ({
|
||||
isMeetingBreakout: AppService.meetingIsBreakout(),
|
||||
breakouts: BreakoutService.getBreakoutsNoTime(),
|
||||
getBreakoutByUser: BreakoutService.getBreakoutByUser,
|
||||
currentBreakoutUser: BreakoutService.getBreakoutUserByUserId(Auth.userID),
|
||||
getBreakoutByUrlData: BreakoutService.getBreakoutByUrlData,
|
||||
currentBreakoutUrlData: BreakoutService.getBreakoutUrlByUserId(Auth.userID),
|
||||
breakoutUserIsIn: BreakoutService.getBreakoutUserIsIn(Auth.userID),
|
||||
}))(BreakoutRoomInvitationContainer);
|
||||
|
@ -10,22 +10,30 @@ import fp from 'lodash/fp';
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
|
||||
const findBreakouts = () => {
|
||||
const BreakoutRooms = Breakouts.find({
|
||||
const BreakoutRooms = Breakouts.find(
|
||||
{
|
||||
parentMeetingId: Auth.meetingID,
|
||||
}, {
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
sequence: 1,
|
||||
},
|
||||
}).fetch();
|
||||
}
|
||||
).fetch();
|
||||
|
||||
return BreakoutRooms;
|
||||
};
|
||||
|
||||
const breakoutRoomUser = (breakoutId) => {
|
||||
const getBreakoutRoomUrl = (breakoutId) => {
|
||||
const breakoutRooms = findBreakouts();
|
||||
const breakoutRoom = breakoutRooms.filter(breakout => breakout.breakoutId === breakoutId).shift();
|
||||
const breakoutUser = breakoutRoom.users?.filter(user => user.userId === Auth.userID).shift();
|
||||
return breakoutUser;
|
||||
const breakoutRoom = breakoutRooms
|
||||
.filter((breakout) => breakout.breakoutId === breakoutId)
|
||||
.shift();
|
||||
const breakoutUrlData =
|
||||
breakoutRoom && breakoutRoom[`url_${Auth.userID}`]
|
||||
? breakoutRoom[`url_${Auth.userID}`]
|
||||
: null;
|
||||
return breakoutUrlData;
|
||||
};
|
||||
|
||||
const endAllBreakouts = () => {
|
||||
@ -47,8 +55,9 @@ const isExtendTimeHigherThanMeetingRemaining = (extendTimeInMinutes) => {
|
||||
|
||||
if (timeRemaining) {
|
||||
const breakoutRooms = findBreakouts();
|
||||
const breakoutRoomsTimeRemaining = (breakoutRooms[0]).timeRemaining;
|
||||
const newBreakoutRoomsRemainingTime = breakoutRoomsTimeRemaining + (extendTimeInMinutes * 60);
|
||||
const breakoutRoomsTimeRemaining = breakoutRooms[0].timeRemaining;
|
||||
const newBreakoutRoomsRemainingTime =
|
||||
breakoutRoomsTimeRemaining + extendTimeInMinutes * 60;
|
||||
// Keep margin of 5 seconds for breakout rooms end before parent meeting
|
||||
const meetingTimeRemainingWithMargin = timeRemaining - 5;
|
||||
|
||||
@ -71,18 +80,24 @@ const extendBreakoutsTime = (extendTimeInMinutes) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const transferUserToMeeting = (fromMeetingId, toMeetingId) => makeCall('transferUser', fromMeetingId, toMeetingId);
|
||||
const transferUserToMeeting = (fromMeetingId, toMeetingId) =>
|
||||
makeCall('transferUser', fromMeetingId, toMeetingId);
|
||||
|
||||
const transferToBreakout = (breakoutId) => {
|
||||
const breakoutRooms = findBreakouts();
|
||||
const breakoutRoom = breakoutRooms.filter(breakout => breakout.breakoutId === breakoutId).shift();
|
||||
const breakoutMeeting = Meetings.findOne({
|
||||
const breakoutRoom = breakoutRooms
|
||||
.filter((breakout) => breakout.breakoutId === breakoutId)
|
||||
.shift();
|
||||
const breakoutMeeting = Meetings.findOne(
|
||||
{
|
||||
$and: [
|
||||
{ 'breakoutProps.sequence': breakoutRoom.sequence },
|
||||
{ 'breakoutProps.parentId': breakoutRoom.parentMeetingId },
|
||||
{ 'meetingProp.isBreakout': true },
|
||||
],
|
||||
}, { fields: { meetingId: 1 } });
|
||||
},
|
||||
{ fields: { meetingId: 1 } }
|
||||
);
|
||||
transferUserToMeeting(Auth.meetingID, breakoutMeeting.meetingId);
|
||||
};
|
||||
|
||||
@ -94,48 +109,56 @@ const amIModerator = () => {
|
||||
const checkInviteModerators = () => {
|
||||
const BREAKOUTS_CONFIG = Meteor.settings.public.app.breakouts;
|
||||
|
||||
return !((amIModerator() && !BREAKOUTS_CONFIG.sendInvitationToIncludedModerators));
|
||||
return !(
|
||||
amIModerator() && !BREAKOUTS_CONFIG.sendInvitationToIncludedModerators
|
||||
);
|
||||
};
|
||||
|
||||
const getBreakoutByUserId = userId => Breakouts.find(
|
||||
{ 'users.userId': userId },
|
||||
{ fields: { timeRemaining: 0 } },
|
||||
const getBreakoutByUserId = (userId) =>
|
||||
Breakouts.find(
|
||||
{ [`url_${userId}`]: { $exists: true } },
|
||||
{ fields: { timeRemaining: 0 } }
|
||||
).fetch();
|
||||
|
||||
const getBreakoutByUser = user => Breakouts.findOne({ users: user });
|
||||
const getBreakoutByUrlData = (breakoutUrlData) =>
|
||||
Breakouts.findOne({ [`url_${Auth.userID}`]: breakoutUrlData });
|
||||
|
||||
const getUsersFromBreakouts = breakoutsArray => breakoutsArray
|
||||
.map(breakout => breakout.users)
|
||||
.reduce((acc, usersArray) => [...acc, ...usersArray], []);
|
||||
const getUrlFromBreakouts = (userId) => (breakoutsArray) =>
|
||||
breakoutsArray
|
||||
.map((breakout) => breakout[`url_${userId}`])
|
||||
.reduce((acc, urlDataArray) => acc.concat(urlDataArray), []);
|
||||
|
||||
const filterUserURLs = userId => breakoutUsersArray => breakoutUsersArray
|
||||
.filter(user => user.userId === userId);
|
||||
const getLastURLInserted = (breakoutURLArray) =>
|
||||
breakoutURLArray.sort((a, b) => a.insertedTime - b.insertedTime).pop();
|
||||
|
||||
const getLastURLInserted = breakoutURLArray => breakoutURLArray
|
||||
.sort((a, b) => a.insertedTime - b.insertedTime).pop();
|
||||
|
||||
const getBreakoutUserByUserId = userId => fp.pipe(
|
||||
const getBreakoutUrlByUserId = (userId) =>
|
||||
fp.pipe(
|
||||
getBreakoutByUserId,
|
||||
getUsersFromBreakouts,
|
||||
filterUserURLs(userId),
|
||||
getLastURLInserted,
|
||||
getUrlFromBreakouts(userId),
|
||||
getLastURLInserted
|
||||
)(userId);
|
||||
|
||||
const getBreakouts = () => Breakouts.find({}, { sort: { sequence: 1 } }).fetch();
|
||||
const getBreakoutsNoTime = () => Breakouts.find(
|
||||
const getBreakouts = () =>
|
||||
Breakouts.find({}, { sort: { sequence: 1 } }).fetch();
|
||||
const getBreakoutsNoTime = () =>
|
||||
Breakouts.find(
|
||||
{},
|
||||
{
|
||||
sort: { sequence: 1 },
|
||||
fields: { timeRemaining: 0 },
|
||||
},
|
||||
}
|
||||
).fetch();
|
||||
|
||||
const getBreakoutUserIsIn = userId => Breakouts.findOne({ 'joinedUsers.userId': new RegExp(`^${userId}`) }, { fields: { sequence: 1 } });
|
||||
const getBreakoutUserIsIn = (userId) =>
|
||||
Breakouts.findOne(
|
||||
{ 'joinedUsers.userId': new RegExp(`^${userId}`) },
|
||||
{ fields: { sequence: 1 } }
|
||||
);
|
||||
|
||||
const isUserInBreakoutRoom = (joinedUsers) => {
|
||||
const userId = Auth.userID;
|
||||
|
||||
return !!joinedUsers.find(user => user.userId.startsWith(userId));
|
||||
return !!joinedUsers.find((user) => user.userId.startsWith(userId));
|
||||
};
|
||||
|
||||
export default {
|
||||
@ -144,16 +167,15 @@ export default {
|
||||
extendBreakoutsTime,
|
||||
isExtendTimeHigherThanMeetingRemaining,
|
||||
requestJoinURL,
|
||||
breakoutRoomUser,
|
||||
getBreakoutRoomUrl,
|
||||
transferUserToMeeting,
|
||||
transferToBreakout,
|
||||
meetingId: () => Auth.meetingID,
|
||||
amIModerator,
|
||||
getBreakoutUserByUserId,
|
||||
getBreakoutByUser,
|
||||
getBreakoutUrlByUserId,
|
||||
getBreakoutByUrlData,
|
||||
getBreakouts,
|
||||
getBreakoutsNoTime,
|
||||
getBreakoutByUserId,
|
||||
getBreakoutUserIsIn,
|
||||
sortUsersByName: UserListService.sortUsersByName,
|
||||
isUserInBreakoutRoom,
|
||||
|
@ -12,7 +12,7 @@ const SIZES = [
|
||||
];
|
||||
|
||||
const COLORS = [
|
||||
'default', 'primary', 'danger', 'warning', 'success', 'dark', 'offline',
|
||||
'default', 'primary', 'danger', 'warning', 'success', 'dark', 'offline', 'muted',
|
||||
];
|
||||
|
||||
const propTypes = {
|
||||
|
@ -34,6 +34,10 @@
|
||||
--btn-offline-bg: var(--color-offline);
|
||||
--btn-offline-border: var(--color-offline);
|
||||
|
||||
--btn-muted-color: var(--color-muted);
|
||||
--btn-muted-bg: var(--color-muted-background);
|
||||
--btn-muted-border: var(--color-muted-background);
|
||||
|
||||
--btn-border-size: var(--border-size);
|
||||
--btn-border-radius: var(--border-radius);
|
||||
--btn-font-weight: 600;
|
||||
@ -325,6 +329,10 @@
|
||||
@include button-variant(var(--btn-offline-color), var(--btn-offline-bg), var(--btn-offline-border));
|
||||
}
|
||||
|
||||
.muted {
|
||||
@include button-variant(var(--btn-muted-color), var(--btn-muted-bg), var(--btn-muted-border));
|
||||
}
|
||||
|
||||
/* Styles
|
||||
* ==========
|
||||
*/
|
||||
@ -362,6 +370,10 @@
|
||||
&.offline {
|
||||
@include button-ghost-variant(var(--btn-offline-bg), var(--btn-offline-color));
|
||||
}
|
||||
|
||||
&.muted {
|
||||
@include button-ghost-variant(var(--btn-muted-bg), var(--btn-muted-color));
|
||||
}
|
||||
}
|
||||
|
||||
.circle {
|
||||
|
@ -23,11 +23,11 @@ class Captions extends React.Component {
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const {
|
||||
padId,
|
||||
locale,
|
||||
revs,
|
||||
} = this.props;
|
||||
|
||||
if (padId === nextProps.padId) {
|
||||
if (locale === nextProps.locale) {
|
||||
if (revs === nextProps.revs && !nextState.clear) return false;
|
||||
}
|
||||
return true;
|
||||
@ -124,7 +124,7 @@ class Captions extends React.Component {
|
||||
export default Captions;
|
||||
|
||||
Captions.propTypes = {
|
||||
padId: PropTypes.string.isRequired,
|
||||
locale: PropTypes.string.isRequired,
|
||||
revs: PropTypes.number.isRequired,
|
||||
data: PropTypes.string.isRequired,
|
||||
};
|
||||
|
@ -9,13 +9,13 @@ const CaptionsContainer = props => (
|
||||
|
||||
export default withTracker(() => {
|
||||
const {
|
||||
padId,
|
||||
locale,
|
||||
revs,
|
||||
data,
|
||||
} = CaptionsService.getCaptionsData();
|
||||
|
||||
return {
|
||||
padId,
|
||||
locale,
|
||||
revs,
|
||||
data,
|
||||
};
|
||||
|
@ -58,8 +58,6 @@ const propTypes = {
|
||||
locale: PropTypes.string.isRequired,
|
||||
ownerId: PropTypes.string.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
padId: PropTypes.string.isRequired,
|
||||
readOnlyPadId: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
@ -85,6 +83,7 @@ class Pad extends PureComponent {
|
||||
|
||||
this.state = {
|
||||
listening: false,
|
||||
url: null,
|
||||
};
|
||||
|
||||
const { locale, intl } = props;
|
||||
@ -104,7 +103,13 @@ class Pad extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
componentDidMount() {
|
||||
const { locale } = this.props;
|
||||
|
||||
this.updatePadURL(locale);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
locale,
|
||||
ownerId,
|
||||
@ -122,6 +127,16 @@ class Pad extends PureComponent {
|
||||
}
|
||||
this.recognition.lang = locale;
|
||||
}
|
||||
|
||||
if (prevProps.ownerId !== ownerId || prevProps.locale !== locale) {
|
||||
this.updatePadURL(locale);
|
||||
}
|
||||
}
|
||||
|
||||
updatePadURL(locale) {
|
||||
PadService.getPadId(locale).then(response => {
|
||||
this.setState({ url: PadService.buildPadURL(response) });
|
||||
});
|
||||
}
|
||||
|
||||
handleListen() {
|
||||
@ -212,16 +227,16 @@ class Pad extends PureComponent {
|
||||
const {
|
||||
locale,
|
||||
intl,
|
||||
padId,
|
||||
readOnlyPadId,
|
||||
ownerId,
|
||||
name,
|
||||
layoutContextDispatch,
|
||||
isResizing,
|
||||
} = this.props;
|
||||
|
||||
const { listening } = this.state;
|
||||
const url = PadService.getPadURL(padId, readOnlyPadId, ownerId);
|
||||
const {
|
||||
listening,
|
||||
url,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div className={styles.pad}>
|
||||
|
@ -40,19 +40,14 @@ export default withTracker(() => {
|
||||
const locale = Session.get('captionsLocale');
|
||||
const caption = CaptionsService.getCaptions(locale);
|
||||
const {
|
||||
padId,
|
||||
name,
|
||||
ownerId,
|
||||
readOnlyPadId,
|
||||
} = caption;
|
||||
|
||||
const { name } = caption ? caption.locale : '';
|
||||
|
||||
return {
|
||||
locale,
|
||||
name,
|
||||
ownerId,
|
||||
padId,
|
||||
readOnlyPadId,
|
||||
currentUserId: Auth.userID,
|
||||
amIModerator: CaptionsService.amIModerator(),
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
|
||||
const NOTE_CONFIG = Meteor.settings.public.note;
|
||||
@ -21,18 +22,19 @@ const getPadParams = () => {
|
||||
return params.join('&');
|
||||
};
|
||||
|
||||
const getPadURL = (padId, readOnlyPadId, ownerId) => {
|
||||
const userId = Auth.userID;
|
||||
const getPadId = (locale) => makeCall('getPadId', locale);
|
||||
|
||||
const buildPadURL = (padId) => {
|
||||
if (padId) {
|
||||
const params = getPadParams();
|
||||
let url;
|
||||
if (!ownerId || userId === ownerId) {
|
||||
url = Auth.authenticateURL(`${NOTE_CONFIG.url}/p/${padId}?${params}`);
|
||||
} else {
|
||||
url = Auth.authenticateURL(`${NOTE_CONFIG.url}/p/${readOnlyPadId}?${params}`);
|
||||
}
|
||||
const url = Auth.authenticateURL(`${NOTE_CONFIG.url}/p/${padId}?${params}`);
|
||||
return url;
|
||||
}
|
||||
|
||||
return null;;
|
||||
};
|
||||
|
||||
export default {
|
||||
getPadURL,
|
||||
getPadId,
|
||||
buildPadURL,
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user