Merge branch 'develop' into activity-report
This commit is contained in:
commit
b0f9835ffd
@ -17,6 +17,7 @@ trait SetPresenterInPodReqMsgHdlr {
|
||||
): MeetingState2x = {
|
||||
if (msg.body.podId == PresentationPod.DEFAULT_PRESENTATION_POD) {
|
||||
// Swith presenter as default presenter pod has changed.
|
||||
log.info("Presenter pod change will trigger a presenter change")
|
||||
AssignPresenterActionHandler.handleAction(liveMeeting, bus.outGW, msg.header.userId, msg.body.nextPresenterId)
|
||||
}
|
||||
SetPresenterInPodActionHandler.handleAction(state, liveMeeting, bus.outGW, msg.header.userId, msg.body.podId, msg.body.nextPresenterId)
|
||||
@ -74,4 +75,4 @@ object SetPresenterInPodActionHandler extends RightsManagementTrait {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ trait AssignPresenterReqMsgHdlr extends RightsManagementTrait {
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleAssignPresenterReqMsg(msg: AssignPresenterReqMsg, state: MeetingState2x): MeetingState2x = {
|
||||
log.info("handleAssignPresenterReqMsg: assignedBy={} newPresenterId={}", msg.body.assignedBy, msg.body.newPresenterId)
|
||||
AssignPresenterActionHandler.handleAction(liveMeeting, outGW, msg.body.assignedBy, msg.body.newPresenterId)
|
||||
|
||||
// Change presenter of default presentation pod
|
||||
@ -68,11 +69,13 @@ object AssignPresenterActionHandler extends RightsManagementTrait {
|
||||
for {
|
||||
oldPres <- Users2x.findPresenter(liveMeeting.users2x)
|
||||
} yield {
|
||||
// Stop external video if it's running
|
||||
ExternalVideoModel.stop(outGW, liveMeeting)
|
||||
if (oldPres.intId != newPresenterId) {
|
||||
// Stop external video if it's running
|
||||
ExternalVideoModel.stop(outGW, liveMeeting)
|
||||
|
||||
Users2x.makeNotPresenter(liveMeeting.users2x, oldPres.intId)
|
||||
broadcastOldPresenterChange(oldPres)
|
||||
Users2x.makeNotPresenter(liveMeeting.users2x, oldPres.intId)
|
||||
broadcastOldPresenterChange(oldPres)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
|
@ -62,6 +62,7 @@ object UsersApp {
|
||||
moderator <- Users2x.findModerator(liveMeeting.users2x)
|
||||
newPresenter <- Users2x.makePresenter(liveMeeting.users2x, moderator.intId)
|
||||
} yield {
|
||||
// println(s"automaticallyAssignPresenter: moderator=${moderator} newPresenter=${newPresenter.intId}");
|
||||
sendPresenterAssigned(outGW, meetingId, newPresenter.intId, newPresenter.name, newPresenter.intId)
|
||||
}
|
||||
}
|
||||
@ -115,6 +116,7 @@ object UsersApp {
|
||||
sendUserEjectedMessageToClient(outGW, meetingId, userId, ejectedBy, reason, reasonCode)
|
||||
sendUserLeftMeetingToAllClients(outGW, meetingId, userId)
|
||||
if (user.presenter) {
|
||||
// println(s"ejectUserFromMeeting will cause a automaticallyAssignPresenter for user=${user}")
|
||||
automaticallyAssignPresenter(outGW, liveMeeting)
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +84,7 @@ trait HandlerHelpers extends SystemConfiguration {
|
||||
outGW.send(event)
|
||||
val newState = startRecordingIfAutoStart2x(outGW, liveMeeting, state)
|
||||
if (!Users2x.hasPresenter(liveMeeting.users2x)) {
|
||||
// println(s"userJoinMeeting will trigger an automaticallyAssignPresenter for user=${newUser}")
|
||||
UsersApp.automaticallyAssignPresenter(outGW, liveMeeting)
|
||||
}
|
||||
newState.update(newState.expiryTracker.setUserHasJoined())
|
||||
|
@ -872,6 +872,7 @@ class MeetingActor(
|
||||
outGW.send(userLeftMeetingEvent)
|
||||
|
||||
if (u.presenter) {
|
||||
log.info("removeUsersWithExpiredUserLeftFlag will cause an automaticallyAssignPresenter because user={} left", u)
|
||||
UsersApp.automaticallyAssignPresenter(outGW, liveMeeting)
|
||||
|
||||
// request screenshare to end
|
||||
|
@ -31,7 +31,7 @@ public class CreateMeeting extends RequestWithChecksum<CreateMeeting.Params> {
|
||||
@MeetingIDConstraint
|
||||
private String meetingID;
|
||||
|
||||
@NotEmpty(message = "You must provide a voice bridge")
|
||||
//@NotEmpty(message = "You must provide a voice bridge")
|
||||
@IsIntegralConstraint(message = "Voice bridge must be a 5-digit integral value")
|
||||
private String voiceBridgeString;
|
||||
private Integer voiceBridge;
|
||||
@ -42,12 +42,12 @@ public class CreateMeeting extends RequestWithChecksum<CreateMeeting.Params> {
|
||||
@PasswordConstraint
|
||||
private String moderatorPW;
|
||||
|
||||
@NotEmpty(message = "You must provide whether this meeting is breakout room")
|
||||
//@NotEmpty(message = "You must provide whether this meeting is breakout room")
|
||||
@IsBooleanConstraint(message = "You must provide a boolean value (true or false) for the breakout room")
|
||||
private String isBreakoutRoomString;
|
||||
private Boolean isBreakoutRoom;
|
||||
|
||||
@NotEmpty(message = "You must provide whether to record this meeting")
|
||||
//@NotEmpty(message = "You must provide whether to record this meeting")
|
||||
@IsBooleanConstraint(message = "Record must be a boolean value (true or false)")
|
||||
private String recordString;
|
||||
private Boolean record;
|
||||
@ -133,7 +133,9 @@ public class CreateMeeting extends RequestWithChecksum<CreateMeeting.Params> {
|
||||
|
||||
@Override
|
||||
public void convertParamsFromString() {
|
||||
voiceBridge = Integer.parseInt(voiceBridgeString);
|
||||
if (voiceBridge != null) {
|
||||
voiceBridge = Integer.parseInt(voiceBridgeString);
|
||||
}
|
||||
isBreakoutRoom = Boolean.parseBoolean(isBreakoutRoomString);
|
||||
record = Boolean.parseBoolean(recordString);
|
||||
}
|
||||
|
@ -169,6 +169,22 @@
|
||||
content: "\e91b";
|
||||
}
|
||||
|
||||
.icon-bbb-volume_down:before {
|
||||
content: "\e947";
|
||||
}
|
||||
|
||||
.icon-bbb-volume_mute:before {
|
||||
content: "\e947";
|
||||
}
|
||||
|
||||
.icon-bbb-volume_off:before {
|
||||
content: "\e947";
|
||||
}
|
||||
|
||||
.icon-bbb-volume_up:before {
|
||||
content: "\e947";
|
||||
}
|
||||
|
||||
/* Aliases for emoji status */
|
||||
.icon-bbb-time:before {
|
||||
content: "\e908";
|
||||
|
@ -726,17 +726,23 @@ class SIPSession {
|
||||
}
|
||||
|
||||
onIceGatheringStateChange(event) {
|
||||
const secondsToGatherIce = (new Date() - this._sessionStartTime) / 1000;
|
||||
|
||||
const iceGatheringState = event.target
|
||||
? event.target.iceGatheringState
|
||||
: null;
|
||||
|
||||
if ((iceGatheringState === 'gathering') && (!this._iceGatheringStartTime)) {
|
||||
this._iceGatheringStartTime = new Date();
|
||||
}
|
||||
|
||||
if (iceGatheringState === 'complete') {
|
||||
const secondsToGatherIce = (new Date()
|
||||
- (this._iceGatheringStartTime || this._sessionStartTime)) / 1000;
|
||||
|
||||
logger.info({
|
||||
logCode: 'sip_js_ice_gathering_time',
|
||||
extraInfo: {
|
||||
callerIdName: this.user.callerIdName,
|
||||
secondsToGatherIce,
|
||||
},
|
||||
}, `ICE gathering candidates took (s): ${secondsToGatherIce}`);
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function handleMeetingEnd({ header, body }) {
|
||||
check(body, Object);
|
||||
const { meetingId } = body;
|
||||
const { meetingId, reason } = body;
|
||||
check(meetingId, String);
|
||||
|
||||
check(header, Object);
|
||||
@ -24,7 +24,7 @@ export default function handleMeetingEnd({ header, body }) {
|
||||
};
|
||||
|
||||
Meetings.update({ meetingId },
|
||||
{ $set: { meetingEnded: true, meetingEndedBy: userId } },
|
||||
{ $set: { meetingEnded: true, meetingEndedBy: userId, meetingEndedReason: reason } },
|
||||
(err, num) => { cb(err, num, 'Meeting'); });
|
||||
|
||||
Breakouts.update({ parentMeetingId: meetingId },
|
||||
|
@ -80,10 +80,7 @@ class Base extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { animations, newLayoutContextState, newLayoutContextDispatch } = this.props;
|
||||
const { input } = newLayoutContextState;
|
||||
const { sidebarContent } = input;
|
||||
const { sidebarContentPanel } = sidebarContent;
|
||||
const { animations } = this.props;
|
||||
|
||||
const {
|
||||
userID: localUserId,
|
||||
@ -141,53 +138,6 @@ class Base extends Component {
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!sidebarContentPanel || Session.equals('subscriptionsReady', true)) {
|
||||
if (!checkedUserSettings) {
|
||||
if (getFromUserSettings('bbb_show_participants_on_login', Meteor.settings.public.layout.showParticipantsOnLogin) && !deviceInfo.isPhone) {
|
||||
if (CHAT_ENABLED && getFromUserSettings('bbb_show_public_chat_on_login', !Meteor.settings.public.chat.startClosed)) {
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_NAVIGATION_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.CHAT,
|
||||
});
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_ID_CHAT_OPEN,
|
||||
value: PUBLIC_CHAT_ID,
|
||||
});
|
||||
} else {
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_NAVIGATION_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_NAVIGATION_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (Session.equals('subscriptionsReady', true)) {
|
||||
checkedUserSettings = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
@ -200,6 +150,7 @@ class Base extends Component {
|
||||
subscriptionsReady,
|
||||
layoutContextDispatch,
|
||||
newLayoutContextDispatch,
|
||||
newLayoutContextState,
|
||||
usersVideo,
|
||||
} = this.props;
|
||||
const {
|
||||
@ -207,6 +158,10 @@ class Base extends Component {
|
||||
meetingExisted,
|
||||
} = this.state;
|
||||
|
||||
const { input } = newLayoutContextState;
|
||||
const { sidebarContent } = input;
|
||||
const { sidebarContentPanel } = sidebarContent;
|
||||
|
||||
if (usersVideo !== prevProps.usersVideo) {
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_NUM_CAMERAS,
|
||||
@ -258,6 +213,53 @@ class Base extends Component {
|
||||
if (enabled) HTML.classList.remove('animationsEnabled');
|
||||
HTML.classList.add('animationsDisabled');
|
||||
}
|
||||
|
||||
if (sidebarContentPanel === PANELS.NONE || Session.equals('subscriptionsReady', true)) {
|
||||
if (!checkedUserSettings) {
|
||||
if (getFromUserSettings('bbb_show_participants_on_login', Meteor.settings.public.layout.showParticipantsOnLogin) && !deviceInfo.isPhone) {
|
||||
if (CHAT_ENABLED && getFromUserSettings('bbb_show_public_chat_on_login', !Meteor.settings.public.chat.startClosed)) {
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_NAVIGATION_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.CHAT,
|
||||
});
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_ID_CHAT_OPEN,
|
||||
value: PUBLIC_CHAT_ID,
|
||||
});
|
||||
} else {
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_NAVIGATION_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_NAVIGATION_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (Session.equals('subscriptionsReady', true)) {
|
||||
checkedUserSettings = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -286,6 +288,7 @@ class Base extends Component {
|
||||
ejectedReason,
|
||||
meetingExist,
|
||||
meetingHasEnded,
|
||||
meetingEndedReason,
|
||||
meetingIsBreakout,
|
||||
subscriptionsReady,
|
||||
User,
|
||||
@ -296,7 +299,7 @@ class Base extends Component {
|
||||
}
|
||||
|
||||
if (ejected) {
|
||||
return (<MeetingEnded code="403" reason={ejectedReason} />);
|
||||
return (<MeetingEnded code="403" ejectedReason={ejectedReason} />);
|
||||
}
|
||||
|
||||
if ((meetingHasEnded || User?.loggedOut) && meetingIsBreakout) {
|
||||
@ -305,7 +308,7 @@ class Base extends Component {
|
||||
}
|
||||
|
||||
if (((meetingHasEnded && !meetingIsBreakout)) || (codeError && User?.loggedOut)) {
|
||||
return (<MeetingEnded code={codeError} />);
|
||||
return (<MeetingEnded code={codeError} endedReason={meetingEndedReason} ejectedReason={ejectedReason} />);
|
||||
}
|
||||
|
||||
if (codeError && !meetingHasEnded) {
|
||||
@ -377,6 +380,7 @@ const BaseContainer = withTracker(() => {
|
||||
const meeting = Meetings.findOne({ meetingId }, {
|
||||
fields: {
|
||||
meetingEnded: 1,
|
||||
meetingEndedReason: 1,
|
||||
meetingProp: 1,
|
||||
},
|
||||
});
|
||||
@ -388,6 +392,7 @@ const BaseContainer = withTracker(() => {
|
||||
const approved = User?.approved && User?.guest;
|
||||
const ejected = User?.ejected;
|
||||
const ejectedReason = User?.ejectedReason;
|
||||
const meetingEndedReason = meeting?.meetingEndedReason;
|
||||
|
||||
let userSubscriptionHandler;
|
||||
|
||||
@ -466,6 +471,7 @@ const BaseContainer = withTracker(() => {
|
||||
isMeteorConnected: Meteor.status().connected,
|
||||
meetingExist: !!meeting,
|
||||
meetingHasEnded: !!meeting && meeting.meetingEnded,
|
||||
meetingEndedReason,
|
||||
meetingIsBreakout: AppService.meetingIsBreakout(),
|
||||
subscriptionsReady: Session.get('subscriptionsReady'),
|
||||
loggedIn,
|
||||
|
@ -34,6 +34,7 @@ class ActionsBar extends PureComponent {
|
||||
setEmojiStatus,
|
||||
currentUser,
|
||||
shortcuts,
|
||||
newLayoutContextDispatch,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@ -81,6 +82,7 @@ class ActionsBar extends PureComponent {
|
||||
? (
|
||||
<PresentationOptionsContainer
|
||||
toggleSwapLayout={toggleSwapLayout}
|
||||
newLayoutContextDispatch={newLayoutContextDispatch}
|
||||
isThereCurrentPresentation={isThereCurrentPresentation}
|
||||
/>
|
||||
)
|
||||
|
@ -12,6 +12,7 @@ import Service from './service';
|
||||
import UserListService from '/imports/ui/components/user-list/service';
|
||||
import ExternalVideoService from '/imports/ui/components/external-video-player/service';
|
||||
import CaptionsService from '/imports/ui/components/captions/service';
|
||||
import { NLayoutContext } from '../layout/context/context';
|
||||
|
||||
import MediaService, {
|
||||
getSwapLayout,
|
||||
@ -21,6 +22,8 @@ import MediaService, {
|
||||
const ActionsBarContainer = (props) => {
|
||||
const usingUsersContext = useContext(UsersContext);
|
||||
const { users } = usingUsersContext;
|
||||
const newLayoutContext = useContext(NLayoutContext);
|
||||
const { newLayoutContextDispatch } = newLayoutContext;
|
||||
|
||||
const currentUser = { userId: Auth.userID, emoji: users[Auth.meetingID][Auth.userID].emoji };
|
||||
|
||||
@ -29,6 +32,7 @@ const ActionsBarContainer = (props) => {
|
||||
...{
|
||||
...props,
|
||||
currentUser,
|
||||
newLayoutContextDispatch,
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
@ -287,6 +287,7 @@ class BreakoutRoom extends PureComponent {
|
||||
roomList.removeEventListener('keydown', this.handleMoveEvent, true);
|
||||
}
|
||||
}
|
||||
this.handleDismiss();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevstate) {
|
||||
|
@ -1,8 +1,16 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import ActionsBarService from '/imports/ui/components/actions-bar/service';
|
||||
|
||||
import CreateBreakoutRoomModal from './component';
|
||||
|
||||
const CreateBreakoutRoomContainer = (props) => (
|
||||
props.amIModerator
|
||||
&& (
|
||||
<CreateBreakoutRoomModal {...props} />
|
||||
)
|
||||
);
|
||||
|
||||
export default withTracker(() => ({
|
||||
createBreakoutRoom: ActionsBarService.createBreakoutRoom,
|
||||
getBreakouts: ActionsBarService.getBreakouts,
|
||||
@ -12,4 +20,5 @@ export default withTracker(() => ({
|
||||
users: ActionsBarService.users(),
|
||||
isMe: ActionsBarService.isMe,
|
||||
meetingName: ActionsBarService.meetingName(),
|
||||
}))(CreateBreakoutRoomModal);
|
||||
amIModerator: ActionsBarService.amIModerator(),
|
||||
}))(CreateBreakoutRoomContainer);
|
||||
|
@ -22,7 +22,12 @@ const intlMessages = defineMessages({
|
||||
|
||||
const shouldUnswapLayout = () => MediaService.shouldShowScreenshare() || MediaService.shouldShowExternalVideo();
|
||||
|
||||
const PresentationOptionsContainer = ({ intl, toggleSwapLayout, isThereCurrentPresentation }) => {
|
||||
const PresentationOptionsContainer = ({
|
||||
intl,
|
||||
toggleSwapLayout,
|
||||
isThereCurrentPresentation,
|
||||
newLayoutContextDispatch
|
||||
}) => {
|
||||
if (shouldUnswapLayout()) toggleSwapLayout();
|
||||
return (
|
||||
<Button
|
||||
@ -34,7 +39,7 @@ const PresentationOptionsContainer = ({ intl, toggleSwapLayout, isThereCurrentPr
|
||||
hideLabel
|
||||
circle
|
||||
size="lg"
|
||||
onClick={toggleSwapLayout}
|
||||
onClick={() => toggleSwapLayout(newLayoutContextDispatch)}
|
||||
id="restore-presentation"
|
||||
disabled={!isThereCurrentPresentation}
|
||||
/>
|
||||
|
@ -78,28 +78,30 @@ const AppContainer = (props) => {
|
||||
const sidebarNavigationIsOpen = sidebarNavigation.isOpen;
|
||||
const sidebarContentIsOpen = sidebarContent.isOpen;
|
||||
|
||||
return currentUserId ?
|
||||
<App
|
||||
{...{
|
||||
actionsbar,
|
||||
actionsBarStyle,
|
||||
currentUserId,
|
||||
media,
|
||||
layoutType,
|
||||
layoutLoaded,
|
||||
meetingLayout,
|
||||
settingsLayout,
|
||||
pushLayoutToEveryone,
|
||||
deviceType,
|
||||
newLayoutContextDispatch,
|
||||
sidebarNavPanel,
|
||||
sidebarNavigationIsOpen,
|
||||
sidebarContentPanel,
|
||||
sidebarContentIsOpen,
|
||||
}}
|
||||
{...otherProps}
|
||||
/>
|
||||
: null
|
||||
return currentUserId
|
||||
? (
|
||||
<App
|
||||
{...{
|
||||
actionsbar,
|
||||
actionsBarStyle,
|
||||
currentUserId,
|
||||
media,
|
||||
layoutType,
|
||||
layoutLoaded,
|
||||
meetingLayout,
|
||||
settingsLayout,
|
||||
pushLayoutToEveryone,
|
||||
deviceType,
|
||||
newLayoutContextDispatch,
|
||||
sidebarNavPanel,
|
||||
sidebarNavigationIsOpen,
|
||||
sidebarContentPanel,
|
||||
sidebarContentIsOpen,
|
||||
}}
|
||||
{...otherProps}
|
||||
/>
|
||||
)
|
||||
: null;
|
||||
};
|
||||
|
||||
const currentUserEmoji = (currentUser) => (currentUser
|
||||
@ -163,9 +165,9 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
|
||||
const layoutManagerLoaded = Session.get('layoutManagerLoaded');
|
||||
const AppSettings = Settings.application;
|
||||
const { viewScreenshare } = Settings.dataSaving;
|
||||
const shouldShowScreenshare = MediaService.shouldShowScreenshare()
|
||||
&& (viewScreenshare || MediaService.isUserPresenter());
|
||||
const shouldShowExternalVideo = MediaService.shouldShowExternalVideo();
|
||||
const shouldShowScreenshare = MediaService.shouldShowScreenshare()
|
||||
&& (viewScreenshare || MediaService.isUserPresenter()) && !shouldShowExternalVideo;
|
||||
|
||||
return {
|
||||
captions: CaptionsService.isCaptionsActive() ? <CaptionsContainer /> : null,
|
||||
|
@ -285,13 +285,14 @@ class AudioModal extends Component {
|
||||
handleJoinListenOnly() {
|
||||
const {
|
||||
joinListenOnly,
|
||||
isConnecting,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
disableActions,
|
||||
} = this.state;
|
||||
|
||||
if (disableActions) return;
|
||||
if (disableActions && isConnecting) return;
|
||||
|
||||
this.setState({
|
||||
disableActions: true,
|
||||
@ -313,13 +314,14 @@ class AudioModal extends Component {
|
||||
handleJoinMicrophone() {
|
||||
const {
|
||||
joinMicrophone,
|
||||
isConnecting,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
disableActions,
|
||||
} = this.state;
|
||||
|
||||
if (disableActions) return;
|
||||
if (disableActions && isConnecting) return;
|
||||
|
||||
this.setState({
|
||||
hasError: false,
|
||||
|
@ -101,6 +101,7 @@ class BreakoutRoom extends PureComponent {
|
||||
this.resetExtendTimeForm = this.resetExtendTimeForm.bind(this);
|
||||
this.renderUserActions = this.renderUserActions.bind(this);
|
||||
this.returnBackToMeeeting = this.returnBackToMeeeting.bind(this);
|
||||
this.closePanel = this.closePanel.bind(this);
|
||||
this.state = {
|
||||
requestedBreakoutId: '',
|
||||
waiting: false,
|
||||
@ -118,6 +119,7 @@ class BreakoutRoom extends PureComponent {
|
||||
setBreakoutAudioTransferStatus,
|
||||
isMicrophoneUser,
|
||||
isReconnecting,
|
||||
breakoutRooms,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -126,6 +128,10 @@ class BreakoutRoom extends PureComponent {
|
||||
joinedAudioOnly,
|
||||
} = this.state;
|
||||
|
||||
if (breakoutRooms.length === 0) {
|
||||
return this.closePanel();
|
||||
}
|
||||
|
||||
if (waiting) {
|
||||
const breakoutUser = breakoutRoomUser(requestedBreakoutId);
|
||||
|
||||
@ -200,6 +206,19 @@ class BreakoutRoom extends PureComponent {
|
||||
this.setState({ joinedAudioOnly: false, breakoutId });
|
||||
}
|
||||
|
||||
closePanel() {
|
||||
const { newLayoutContextDispatch } = this.props;
|
||||
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.NONE,
|
||||
});
|
||||
}
|
||||
|
||||
renderUserActions(breakoutId, joinedUsers, number) {
|
||||
const {
|
||||
isMicrophoneUser,
|
||||
@ -459,7 +478,6 @@ class BreakoutRoom extends PureComponent {
|
||||
intl,
|
||||
endAllBreakouts,
|
||||
amIModerator,
|
||||
newLayoutContextDispatch,
|
||||
} = this.props;
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
@ -469,14 +487,7 @@ class BreakoutRoom extends PureComponent {
|
||||
aria-label={intl.formatMessage(intlMessages.breakoutAriaTitle)}
|
||||
className={styles.header}
|
||||
onClick={() => {
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.NONE,
|
||||
});
|
||||
this.closePanel();
|
||||
}}
|
||||
/>
|
||||
{this.renderBreakoutRooms()}
|
||||
@ -491,14 +502,7 @@ class BreakoutRoom extends PureComponent {
|
||||
label={intl.formatMessage(intlMessages.endAllBreakouts)}
|
||||
className={styles.endButton}
|
||||
onClick={() => {
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.NONE,
|
||||
});
|
||||
this.closePanel();
|
||||
endAllBreakouts();
|
||||
}}
|
||||
/>
|
||||
|
@ -34,8 +34,6 @@ const openBreakoutJoinConfirmation = (breakout, breakoutName, mountModal) => mou
|
||||
/>,
|
||||
);
|
||||
|
||||
const closeBreakoutJoinConfirmation = mountModal => mountModal(null);
|
||||
|
||||
class BreakoutRoomInvitation extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -57,7 +55,6 @@ class BreakoutRoomInvitation extends Component {
|
||||
checkBreakouts(oldProps) {
|
||||
const {
|
||||
breakouts,
|
||||
mountModal,
|
||||
currentBreakoutUser,
|
||||
getBreakoutByUser,
|
||||
breakoutUserIsIn,
|
||||
@ -67,11 +64,7 @@ class BreakoutRoomInvitation extends Component {
|
||||
didSendBreakoutInvite,
|
||||
} = this.state;
|
||||
|
||||
const hadBreakouts = oldProps.breakouts.length > 0;
|
||||
const hasBreakouts = breakouts.length > 0;
|
||||
if (!hasBreakouts && hadBreakouts) {
|
||||
closeBreakoutJoinConfirmation(mountModal);
|
||||
}
|
||||
|
||||
if (hasBreakouts && !breakoutUserIsIn && BreakoutService.checkInviteModerators()) {
|
||||
// Have to check for freeJoin breakouts first because currentBreakoutUser will
|
||||
|
@ -6,6 +6,7 @@ import Button from '/imports/ui/components/button/component';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import PadService from './service';
|
||||
import CaptionsService from '/imports/ui/components/captions/service';
|
||||
import { notify } from '/imports/ui/services/notification';
|
||||
import { styles } from './styles';
|
||||
import { PANELS, ACTIONS } from '../../layout/enums';
|
||||
|
||||
@ -46,6 +47,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.captions.pad.dictationOffDesc',
|
||||
description: 'Aria description for button that turns off speech recognition',
|
||||
},
|
||||
speechRecognitionStop: {
|
||||
id: 'app.captions.pad.speechRecognitionStop',
|
||||
description: 'Notification for stopped speech recognition',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
@ -75,11 +80,19 @@ class Pad extends PureComponent {
|
||||
listening: false,
|
||||
};
|
||||
|
||||
const { locale } = props;
|
||||
const { locale, intl } = props;
|
||||
this.recognition = CaptionsService.initSpeechRecognition(locale);
|
||||
|
||||
this.toggleListen = this.toggleListen.bind(this);
|
||||
this.handleListen = this.handleListen.bind(this);
|
||||
|
||||
this.recognition.addEventListener('end', () => {
|
||||
const { listening } = this.state;
|
||||
if (listening) {
|
||||
notify(intl.formatMessage(intlMessages.speechRecognitionStop), 'info', 'warning');
|
||||
this.stopListen();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
@ -167,6 +180,10 @@ class Pad extends PureComponent {
|
||||
listening: !listening,
|
||||
}, this.handleListen);
|
||||
}
|
||||
|
||||
stopListen() {
|
||||
this.setState({ listening: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
|
@ -263,6 +263,11 @@ const exportChat = (timeWindowList, users, intl) => {
|
||||
const hour = date.getHours().toString().padStart(2, 0);
|
||||
const min = date.getMinutes().toString().padStart(2, 0);
|
||||
const hourMin = `[${hour}:${min}]`;
|
||||
|
||||
// Skip the reduce aggregation for the sync messages because they aren't localized, causing an error in line 268
|
||||
// Also they're temporary (preliminary) messages, so it doesn't make sense export them
|
||||
if (['SYSTEM_MESSAGE-sync-msg', 'synced'].includes(message.id)) return acc;
|
||||
|
||||
let userName = message.id.startsWith(SYSTEM_CHAT_TYPE)
|
||||
? ''
|
||||
: `${users[timeWindow.sender].name}: `;
|
||||
|
@ -16,6 +16,7 @@ const CHAT_CLEAR_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_clear;
|
||||
const ITENS_PER_PAGE = CHAT_CONFIG.itemsPerPage;
|
||||
const TIME_BETWEEN_FETCHS = CHAT_CONFIG.timeBetweenFetchs;
|
||||
const EVENT_NAME = 'bbb-group-chat-messages-subscription-has-stoppped';
|
||||
const EVENT_NAME_SUBSCRIPTION_READY = 'bbb-group-chat-messages-subscriptions-ready';
|
||||
|
||||
const getMessagesBeforeJoinCounter = async () => {
|
||||
const counter = await makeCall('chatMessageBeforeJoinCounter');
|
||||
@ -24,7 +25,8 @@ const getMessagesBeforeJoinCounter = async () => {
|
||||
|
||||
const startSyncMessagesbeforeJoin = async (dispatch) => {
|
||||
const chatsMessagesCount = await getMessagesBeforeJoinCounter();
|
||||
const pagesPerChat = chatsMessagesCount.map(chat => ({ ...chat, pages: Math.ceil(chat.count / ITENS_PER_PAGE), syncedPages: 0 }));
|
||||
const pagesPerChat = chatsMessagesCount
|
||||
.map((chat) => ({ ...chat, pages: Math.ceil(chat.count / ITENS_PER_PAGE), syncedPages: 0 }));
|
||||
|
||||
const syncRoutine = async (chatsToSync) => {
|
||||
if (!chatsToSync.length) return;
|
||||
@ -49,9 +51,8 @@ const startSyncMessagesbeforeJoin = async (dispatch) => {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
await new Promise(r => setTimeout(r, TIME_BETWEEN_FETCHS));
|
||||
syncRoutine(pagesToFetch.filter(chat => !(chat.syncedPages > chat.pages)));
|
||||
await new Promise((r) => setTimeout(r, TIME_BETWEEN_FETCHS));
|
||||
syncRoutine(pagesToFetch.filter((chat) => !(chat.syncedPages > chat.pages)));
|
||||
};
|
||||
syncRoutine(pagesPerChat);
|
||||
};
|
||||
@ -62,6 +63,7 @@ const Adapter = () => {
|
||||
const usingUsersContext = useContext(UsersContext);
|
||||
const { users } = usingUsersContext;
|
||||
const [syncStarted, setSync] = useState(true);
|
||||
const [subscriptionReady, setSubscriptionReady] = useState(false);
|
||||
ChatLogger.trace('chatAdapter::body::users', users[Auth.meetingID]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -74,22 +76,26 @@ const Adapter = () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener(EVENT_NAME_SUBSCRIPTION_READY, () => {
|
||||
setSubscriptionReady(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const connectionStatus = Meteor.status();
|
||||
if (connectionStatus.connected && !syncStarted && Auth.userID) {
|
||||
setSync(true);
|
||||
|
||||
startSyncMessagesbeforeJoin(dispatch);
|
||||
if (connectionStatus.connected && !syncStarted && Auth.userID && subscriptionReady) {
|
||||
setTimeout(() => {
|
||||
setSync(true);
|
||||
startSyncMessagesbeforeJoin(dispatch);
|
||||
}, 1000);
|
||||
}
|
||||
}, [Meteor.status().connected, syncStarted, Auth.userID]);
|
||||
|
||||
}, [Meteor.status().connected, syncStarted, Auth.userID, subscriptionReady]);
|
||||
|
||||
/* needed to prevent an issue with dupĺicated messages when user role is changed
|
||||
more info: https://github.com/bigbluebutton/bigbluebutton/issues/11842 */
|
||||
useEffect(() => {
|
||||
if (users[Auth.meetingID] && users[Auth.meetingID][Auth.userID]) {
|
||||
if (users[Auth.meetingID]) {
|
||||
if (currentUserData?.role !== users[Auth.meetingID][Auth.userID].role) {
|
||||
prevUserData = currentUserData;
|
||||
}
|
||||
@ -114,7 +120,7 @@ const Adapter = () => {
|
||||
}, 1000, { trailing: true, leading: true });
|
||||
|
||||
Meteor.connection._stream.socket.addEventListener('message', (msg) => {
|
||||
if (msg.data.indexOf('{"msg":"added","collection":"group-chat-msg"') != -1) {
|
||||
if (msg.data.indexOf('{"msg":"added","collection":"group-chat-msg"') !== -1) {
|
||||
const parsedMsg = JSON.parse(msg.data);
|
||||
if (parsedMsg.msg === 'added') {
|
||||
const { fields } = parsedMsg;
|
||||
|
@ -6,6 +6,9 @@ import { sendMessage, onMessage, removeAllListeners } from './service';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import Service from './service';
|
||||
|
||||
import VolumeSlider from './volume-slider/component';
|
||||
import ReloadButton from '/imports/ui/components/reload-button/component';
|
||||
|
||||
import ArcPlayer from './custom-players/arc-player';
|
||||
import PeerTubePlayer from './custom-players/peertube';
|
||||
|
||||
@ -16,6 +19,9 @@ const intlMessages = defineMessages({
|
||||
id: 'app.externalVideo.autoPlayWarning',
|
||||
description: 'Shown when user needs to interact with player to make it work',
|
||||
},
|
||||
refreshLabel: {
|
||||
id: 'app.externalVideo.refreshLabel',
|
||||
},
|
||||
});
|
||||
|
||||
const SYNC_INTERVAL_SECONDS = 5;
|
||||
@ -43,10 +49,12 @@ class VideoPlayer extends Component {
|
||||
this.throttleTimeout = null;
|
||||
|
||||
this.state = {
|
||||
mutedByEchoTest: false,
|
||||
muted: false,
|
||||
playing: false,
|
||||
autoPlayBlocked: false,
|
||||
volume: 1,
|
||||
playbackRate: 1,
|
||||
key: 0,
|
||||
};
|
||||
|
||||
this.opts = {
|
||||
@ -54,18 +62,18 @@ class VideoPlayer extends Component {
|
||||
playerOptions: {
|
||||
autoplay: true,
|
||||
playsinline: true,
|
||||
controls: true,
|
||||
controls: isPresenter,
|
||||
},
|
||||
file: {
|
||||
attributes: {
|
||||
controls: true,
|
||||
autoPlay: true,
|
||||
playsInline: true,
|
||||
controls: isPresenter ? 'controls' : '',
|
||||
autoplay: 'autoplay',
|
||||
playsinline: 'playsinline',
|
||||
},
|
||||
},
|
||||
dailymotion: {
|
||||
params: {
|
||||
controls: true,
|
||||
controls: isPresenter,
|
||||
},
|
||||
},
|
||||
youtube: {
|
||||
@ -75,7 +83,7 @@ class VideoPlayer extends Component {
|
||||
autohide: 1,
|
||||
rel: 0,
|
||||
ecver: 2,
|
||||
controls: isPresenter ? 1 : 2,
|
||||
controls: isPresenter ? 1 : 0,
|
||||
},
|
||||
},
|
||||
peertube: {
|
||||
@ -83,7 +91,7 @@ class VideoPlayer extends Component {
|
||||
},
|
||||
twitch: {
|
||||
options: {
|
||||
controls: true,
|
||||
controls: isPresenter,
|
||||
},
|
||||
playerId: 'externalVideoPlayerTwitch',
|
||||
},
|
||||
@ -95,12 +103,18 @@ class VideoPlayer extends Component {
|
||||
this.clearVideoListeners = this.clearVideoListeners.bind(this);
|
||||
this.handleFirstPlay = this.handleFirstPlay.bind(this);
|
||||
this.handleResize = this.handleResize.bind(this);
|
||||
this.handleReload = this.handleReload.bind(this);
|
||||
this.handleOnProgress = this.handleOnProgress.bind(this);
|
||||
this.handleOnReady = this.handleOnReady.bind(this);
|
||||
this.handleOnPlay = this.handleOnPlay.bind(this);
|
||||
this.handleOnPause = this.handleOnPause.bind(this);
|
||||
this.handleVolumeChanged = this.handleVolumeChanged.bind(this);
|
||||
this.handleOnMuted = this.handleOnMuted.bind(this);
|
||||
this.sendSyncMessage = this.sendSyncMessage.bind(this);
|
||||
this.getCurrentPlaybackRate = this.getCurrentPlaybackRate.bind(this);
|
||||
this.getCurrentTime = this.getCurrentTime.bind(this);
|
||||
this.getCurrentVolume = this.getCurrentVolume.bind(this);
|
||||
this.getMuted = this.getMuted.bind(this);
|
||||
this.setPlaybackRate = this.setPlaybackRate.bind(this);
|
||||
this.resizeListener = () => {
|
||||
setTimeout(this.handleResize, 0);
|
||||
@ -388,7 +402,6 @@ class VideoPlayer extends Component {
|
||||
return logger.error('No player on seek');
|
||||
}
|
||||
|
||||
|
||||
// Seek if viewer has drifted too far away from presenter
|
||||
if (Math.abs(this.getCurrentTime() - time) > SYNC_INTERVAL_SECONDS * 0.75) {
|
||||
player.seekTo(time, true);
|
||||
@ -449,16 +462,53 @@ class VideoPlayer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleOnProgress() {
|
||||
const volume = this.getCurrentVolume();
|
||||
const muted = this.getMuted();
|
||||
|
||||
this.setState({ volume, muted });
|
||||
}
|
||||
|
||||
getMuted() {
|
||||
const intPlayer = this.player && this.player.getInternalPlayer();
|
||||
|
||||
return (intPlayer && intPlayer.isMuted && intPlayer.isMuted()) || this.state.muted;
|
||||
}
|
||||
|
||||
getCurrentVolume() {
|
||||
const intPlayer = this.player && this.player.getInternalPlayer();
|
||||
|
||||
return (intPlayer && intPlayer.getVolume && intPlayer.getVolume() / 100.0) || this.state.volume;
|
||||
}
|
||||
|
||||
handleVolumeChanged(volume) {
|
||||
this.setState({ volume });
|
||||
}
|
||||
|
||||
handleOnMuted(muted) {
|
||||
this.setState({ muted });
|
||||
}
|
||||
|
||||
handleReload() {
|
||||
// increment key and force a re-render of the video component
|
||||
this.setState({key: this.state.key + 1});
|
||||
|
||||
// hack, resize player
|
||||
this.resizeListener();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { videoUrl, intl } = this.props;
|
||||
const { videoUrl, isPresenter, intl } = this.props;
|
||||
const {
|
||||
playing, playbackRate, mutedByEchoTest, autoPlayBlocked,
|
||||
volume, muted, key,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="video-player"
|
||||
data-test="videoPlayer"
|
||||
className={styles.videoPlayerWrapper}
|
||||
ref={(ref) => { this.playerParent = ref; }}
|
||||
>
|
||||
{autoPlayBlocked
|
||||
@ -473,14 +523,32 @@ class VideoPlayer extends Component {
|
||||
className={styles.videoPlayer}
|
||||
url={videoUrl}
|
||||
config={this.opts}
|
||||
muted={mutedByEchoTest}
|
||||
volume={(muted || mutedByEchoTest) ? 0 : volume}
|
||||
muted={muted || mutedByEchoTest}
|
||||
playing={playing}
|
||||
playbackRate={playbackRate}
|
||||
onProgress={this.handleOnProgress}
|
||||
onReady={this.handleOnReady}
|
||||
onPlay={this.handleOnPlay}
|
||||
onPause={this.handleOnPause}
|
||||
key={'react-player' + key}
|
||||
ref={(ref) => { this.player = ref; }}
|
||||
/>
|
||||
{ !isPresenter ?
|
||||
<div className={styles.hoverToolbar}>
|
||||
<VolumeSlider
|
||||
volume={volume}
|
||||
muted={muted || mutedByEchoTest}
|
||||
onMuted={this.handleOnMuted}
|
||||
onVolumeChanged={this.handleVolumeChanged}
|
||||
/>
|
||||
<ReloadButton
|
||||
handleReload={this.handleReload}
|
||||
label={intl.formatMessage(intlMessages.refreshLabel)}>
|
||||
</ReloadButton>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,18 @@
|
||||
@import "/imports/ui/stylesheets/mixins/focus";
|
||||
@import "/imports/ui/stylesheets/variables/_all";
|
||||
|
||||
.hoverToolbar {
|
||||
display: none;
|
||||
|
||||
:hover > & {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.videoPlayerWrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.videoPlayer iframe {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
@ -0,0 +1,92 @@
|
||||
import React, { Component } from "react"
|
||||
import ReactDOM from "react-dom"
|
||||
|
||||
import { styles } from './styles';
|
||||
|
||||
class VolumeSlider extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
volume: props.volume,
|
||||
muted: props.muted,
|
||||
}
|
||||
|
||||
this.handleOnChange = this.handleOnChange.bind(this);
|
||||
this.getVolumeIcon = this.getVolumeIcon.bind(this);
|
||||
this.setMuted = this.setMuted.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProp, prevState) {
|
||||
if (prevProp.volume !== this.props.volume) {
|
||||
this.handleOnChange(this.props.volume);
|
||||
}
|
||||
|
||||
if (prevProp.muted !== this.props.muted) {
|
||||
this.setMuted(this.props.muted);
|
||||
}
|
||||
}
|
||||
|
||||
handleOnChange(volume) {
|
||||
this.props.onVolumeChanged(volume);
|
||||
|
||||
this.setState({ volume }, () => {
|
||||
const { volume, muted } = this.state;
|
||||
if (muted && volume > 0) { // unmute if volume was raised during mute
|
||||
this.setMuted(false);
|
||||
} else if (volume <= 0) { // mute if volume is turned to 0
|
||||
this.setMuted(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setMuted(muted) {
|
||||
this.setState({ muted }, () => {
|
||||
this.props.onMuted(muted);
|
||||
});
|
||||
}
|
||||
|
||||
getVolumeIcon() {
|
||||
const { muted, volume } = this.state;
|
||||
|
||||
if (muted || volume <= 0) {
|
||||
return 'volume_off';
|
||||
} else if (volume <= 0.25) {
|
||||
return 'volume_mute';
|
||||
} else if (volume <= 0.75) {
|
||||
return 'volume_down';
|
||||
} else {
|
||||
return 'volume_up';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { muted, volume } = this.state;
|
||||
const { handleOnChange, setMuted, getVolumeIcon } = this;
|
||||
|
||||
return (
|
||||
<div className={styles.slider}>
|
||||
<span
|
||||
className={styles.volume}
|
||||
onClick={ () => { setMuted(!muted) } }
|
||||
>
|
||||
<i
|
||||
tabIndex="-1"
|
||||
className={`icon-bbb-${getVolumeIcon()}`}>
|
||||
</i>
|
||||
</span>
|
||||
<input
|
||||
className={styles.volumeSlider}
|
||||
type="range"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.02}
|
||||
value={muted ? 0 : volume}
|
||||
onChange={(e) => { handleOnChange(e.target.valueAsNumber) }}
|
||||
/>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
export default VolumeSlider;
|
@ -0,0 +1,47 @@
|
||||
@import "../../../stylesheets/variables/_all";
|
||||
|
||||
%baseIndicator {
|
||||
width: 0.9em;
|
||||
}
|
||||
|
||||
%baseIndicatorLabel {
|
||||
font-size: var(--font-size-base);
|
||||
margin: 0 0 0 0.5rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
max-width: 20vw;
|
||||
}
|
||||
|
||||
.slider {
|
||||
@extend %baseIndicator;
|
||||
display: flex;
|
||||
position: relative;
|
||||
bottom: 3.5em;
|
||||
left: 1em;
|
||||
|
||||
min-width: 200px;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
padding: 0.5em 1em 0.5em 1em;
|
||||
border-radius: 32px;
|
||||
|
||||
i {
|
||||
color: white;
|
||||
transition: 0.5s;
|
||||
font-size: 200%;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.volumeSlider {
|
||||
width: 100%;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.volume {
|
||||
margin-right: 0.5em;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
@ -41,7 +41,7 @@ const DEFAULT_VALUES = {
|
||||
sidebarContentHeight: '100%',
|
||||
sidebarContentTop: 0,
|
||||
sidebarContentTabOrder: 2,
|
||||
sidebarContentPanel: PANELS.CHAT,
|
||||
sidebarContentPanel: PANELS.NONE,
|
||||
};
|
||||
|
||||
export default DEFAULT_VALUES;
|
||||
|
@ -14,13 +14,11 @@ const { isMobile } = deviceInfo;
|
||||
// values based on sass file
|
||||
const USERLIST_MIN_WIDTH = 150;
|
||||
const USERLIST_MAX_WIDTH = 240;
|
||||
const CHAT_MIN_WIDTH = 320;
|
||||
const CHAT_MAX_WIDTH = 400;
|
||||
const PANEL_MIN_WIDTH = 320;
|
||||
const PANEL_MAX_WIDTH = 400;
|
||||
const NAVBAR_HEIGHT = 112;
|
||||
const LARGE_NAVBAR_HEIGHT = 170;
|
||||
const ACTIONSBAR_HEIGHT = isMobile ? 50 : 42;
|
||||
const BREAKOUT_MIN_WIDTH = 320;
|
||||
const BREAKOUT_MAX_WIDTH = 400;
|
||||
|
||||
const WEBCAMSAREA_MIN_PERCENT = 0.2;
|
||||
const WEBCAMSAREA_MAX_PERCENT = 0.8;
|
||||
@ -333,7 +331,7 @@ class LayoutManagerComponent extends Component {
|
||||
};
|
||||
} else if (!storageSecondPanelWidth) {
|
||||
newPanelSize = {
|
||||
width: min(max((this.windowWidth() * 0.2), CHAT_MIN_WIDTH), CHAT_MAX_WIDTH),
|
||||
width: min(max((this.windowWidth() * 0.2), PANEL_MIN_WIDTH), PANEL_MAX_WIDTH),
|
||||
};
|
||||
} else {
|
||||
newPanelSize = {
|
||||
@ -558,14 +556,12 @@ export default withLayoutConsumer(NewLayoutManager.withConsumer(LayoutManagerCom
|
||||
export {
|
||||
USERLIST_MIN_WIDTH,
|
||||
USERLIST_MAX_WIDTH,
|
||||
CHAT_MIN_WIDTH,
|
||||
CHAT_MAX_WIDTH,
|
||||
PANEL_MIN_WIDTH,
|
||||
PANEL_MAX_WIDTH,
|
||||
NAVBAR_HEIGHT,
|
||||
LARGE_NAVBAR_HEIGHT,
|
||||
ACTIONSBAR_HEIGHT,
|
||||
WEBCAMSAREA_MIN_PERCENT,
|
||||
WEBCAMSAREA_MAX_PERCENT,
|
||||
PRESENTATIONAREA_MIN_WIDTH,
|
||||
BREAKOUT_MIN_WIDTH,
|
||||
BREAKOUT_MAX_WIDTH,
|
||||
};
|
||||
|
@ -3,7 +3,9 @@ import _ from 'lodash';
|
||||
import NewLayoutContext from '../context/context';
|
||||
import DEFAULT_VALUES from '../defaultValues';
|
||||
import { INITIAL_INPUT_STATE } from '../context/initState';
|
||||
import { DEVICE_TYPE, ACTIONS, CAMERADOCK_POSITION } from '../enums';
|
||||
import {
|
||||
DEVICE_TYPE, ACTIONS, CAMERADOCK_POSITION, PANELS,
|
||||
} from '../enums';
|
||||
|
||||
const windowWidth = () => window.document.documentElement.clientWidth;
|
||||
const windowHeight = () => window.document.documentElement.clientHeight;
|
||||
@ -168,6 +170,8 @@ class CustomLayout extends Component {
|
||||
}, INITIAL_INPUT_STATE),
|
||||
});
|
||||
} else {
|
||||
const { sidebarContentPanel } = input.sidebarContent;
|
||||
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_LAYOUT_INPUT,
|
||||
value: _.defaultsDeep({
|
||||
@ -175,8 +179,10 @@ class CustomLayout extends Component {
|
||||
isOpen: true,
|
||||
},
|
||||
sidebarContent: {
|
||||
isOpen: deviceType === DEVICE_TYPE.TABLET_LANDSCAPE
|
||||
|| deviceType === DEVICE_TYPE.DESKTOP,
|
||||
isOpen: sidebarContentPanel !== PANELS.NONE
|
||||
&& (deviceType === DEVICE_TYPE.TABLET_LANDSCAPE
|
||||
|| deviceType === DEVICE_TYPE.DESKTOP),
|
||||
sidebarContentPanel,
|
||||
},
|
||||
sidebarContentHorizontalResizer: {
|
||||
isOpen: false,
|
||||
@ -419,166 +425,175 @@ class CustomLayout extends Component {
|
||||
calculatesCameraDockBounds(sidebarNavWidth, sidebarContentWidth, mediaAreaBounds) {
|
||||
const { newLayoutContextState } = this.props;
|
||||
const { input, fullscreen } = newLayoutContextState;
|
||||
const { presentation } = input;
|
||||
const { isOpen } = presentation;
|
||||
|
||||
const cameraDockBounds = {};
|
||||
|
||||
if (input.cameraDock.numCameras > 0) {
|
||||
let cameraDockLeft = 0;
|
||||
let cameraDockHeight = 0;
|
||||
let cameraDockWidth = 0;
|
||||
switch (input.cameraDock.position) {
|
||||
case CAMERADOCK_POSITION.CONTENT_TOP:
|
||||
cameraDockLeft = mediaAreaBounds.left;
|
||||
if (!isOpen) {
|
||||
cameraDockBounds.width = mediaAreaBounds.width;
|
||||
cameraDockBounds.maxWidth = mediaAreaBounds.width;
|
||||
cameraDockBounds.height = mediaAreaBounds.height;
|
||||
cameraDockBounds.maxHeight = mediaAreaBounds.height;
|
||||
} else {
|
||||
let cameraDockLeft = 0;
|
||||
let cameraDockHeight = 0;
|
||||
let cameraDockWidth = 0;
|
||||
switch (input.cameraDock.position) {
|
||||
case CAMERADOCK_POSITION.CONTENT_TOP:
|
||||
cameraDockLeft = mediaAreaBounds.left;
|
||||
|
||||
if (input.cameraDock.height === 0) {
|
||||
if (input.presentation.isOpen) {
|
||||
if (input.cameraDock.height === 0) {
|
||||
if (input.presentation.isOpen) {
|
||||
cameraDockHeight = min(
|
||||
max((mediaAreaBounds.height * 0.2), DEFAULT_VALUES.cameraDockMinHeight),
|
||||
(mediaAreaBounds.height - DEFAULT_VALUES.cameraDockMinHeight),
|
||||
);
|
||||
} else {
|
||||
cameraDockHeight = mediaAreaBounds.height;
|
||||
}
|
||||
} else {
|
||||
cameraDockHeight = min(
|
||||
max((mediaAreaBounds.height * 0.2), DEFAULT_VALUES.cameraDockMinHeight),
|
||||
max(input.cameraDock.height, DEFAULT_VALUES.cameraDockMinHeight),
|
||||
(mediaAreaBounds.height - DEFAULT_VALUES.cameraDockMinHeight),
|
||||
);
|
||||
} else {
|
||||
cameraDockHeight = mediaAreaBounds.height;
|
||||
}
|
||||
} else {
|
||||
cameraDockHeight = min(
|
||||
max(input.cameraDock.height, DEFAULT_VALUES.cameraDockMinHeight),
|
||||
(mediaAreaBounds.height - DEFAULT_VALUES.cameraDockMinHeight),
|
||||
);
|
||||
}
|
||||
|
||||
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight;
|
||||
cameraDockBounds.left = cameraDockLeft;
|
||||
cameraDockBounds.minWidth = mediaAreaBounds.width;
|
||||
cameraDockBounds.width = mediaAreaBounds.width;
|
||||
cameraDockBounds.maxWidth = mediaAreaBounds.width;
|
||||
cameraDockBounds.minHeight = DEFAULT_VALUES.cameraDockMinHeight;
|
||||
cameraDockBounds.height = cameraDockHeight;
|
||||
cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.8;
|
||||
break;
|
||||
case CAMERADOCK_POSITION.CONTENT_RIGHT:
|
||||
if (input.cameraDock.width === 0) {
|
||||
if (input.presentation.isOpen) {
|
||||
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight;
|
||||
cameraDockBounds.left = cameraDockLeft;
|
||||
cameraDockBounds.minWidth = mediaAreaBounds.width;
|
||||
cameraDockBounds.width = mediaAreaBounds.width;
|
||||
cameraDockBounds.maxWidth = mediaAreaBounds.width;
|
||||
cameraDockBounds.minHeight = DEFAULT_VALUES.cameraDockMinHeight;
|
||||
cameraDockBounds.height = cameraDockHeight;
|
||||
cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.8;
|
||||
break;
|
||||
case CAMERADOCK_POSITION.CONTENT_RIGHT:
|
||||
if (input.cameraDock.width === 0) {
|
||||
if (input.presentation.isOpen) {
|
||||
cameraDockWidth = min(
|
||||
max((mediaAreaBounds.width * 0.2), DEFAULT_VALUES.cameraDockMinWidth),
|
||||
(mediaAreaBounds.width - DEFAULT_VALUES.cameraDockMinWidth),
|
||||
);
|
||||
} else {
|
||||
cameraDockWidth = mediaAreaBounds.width;
|
||||
}
|
||||
} else {
|
||||
cameraDockWidth = min(
|
||||
max((mediaAreaBounds.width * 0.2), DEFAULT_VALUES.cameraDockMinWidth),
|
||||
max(input.cameraDock.width, DEFAULT_VALUES.cameraDockMinWidth),
|
||||
(mediaAreaBounds.width - DEFAULT_VALUES.cameraDockMinWidth),
|
||||
);
|
||||
} else {
|
||||
cameraDockWidth = mediaAreaBounds.width;
|
||||
}
|
||||
} else {
|
||||
cameraDockWidth = min(
|
||||
max(input.cameraDock.width, DEFAULT_VALUES.cameraDockMinWidth),
|
||||
(mediaAreaBounds.width - DEFAULT_VALUES.cameraDockMinWidth),
|
||||
);
|
||||
}
|
||||
|
||||
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight;
|
||||
cameraDockBounds.left = input.presentation.isOpen
|
||||
? (mediaAreaBounds.left + mediaAreaBounds.width) - cameraDockWidth
|
||||
: mediaAreaBounds.left;
|
||||
cameraDockBounds.minWidth = DEFAULT_VALUES.cameraDockMinWidth;
|
||||
cameraDockBounds.width = cameraDockWidth;
|
||||
cameraDockBounds.maxWidth = mediaAreaBounds.width * 0.8;
|
||||
cameraDockBounds.minHeight = DEFAULT_VALUES.cameraDockMinHeight;
|
||||
cameraDockBounds.height = mediaAreaBounds.height;
|
||||
cameraDockBounds.maxHeight = mediaAreaBounds.height;
|
||||
break;
|
||||
case CAMERADOCK_POSITION.CONTENT_BOTTOM:
|
||||
cameraDockLeft = mediaAreaBounds.left;
|
||||
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight;
|
||||
cameraDockBounds.left = input.presentation.isOpen
|
||||
? (mediaAreaBounds.left + mediaAreaBounds.width) - cameraDockWidth
|
||||
: mediaAreaBounds.left;
|
||||
cameraDockBounds.minWidth = DEFAULT_VALUES.cameraDockMinWidth;
|
||||
cameraDockBounds.width = cameraDockWidth;
|
||||
cameraDockBounds.maxWidth = mediaAreaBounds.width * 0.8;
|
||||
cameraDockBounds.minHeight = DEFAULT_VALUES.cameraDockMinHeight;
|
||||
cameraDockBounds.height = mediaAreaBounds.height;
|
||||
cameraDockBounds.maxHeight = mediaAreaBounds.height;
|
||||
break;
|
||||
case CAMERADOCK_POSITION.CONTENT_BOTTOM:
|
||||
cameraDockLeft = mediaAreaBounds.left;
|
||||
|
||||
if (input.cameraDock.height === 0) {
|
||||
if (input.presentation.isOpen) {
|
||||
if (input.cameraDock.height === 0) {
|
||||
if (input.presentation.isOpen) {
|
||||
cameraDockHeight = min(
|
||||
max((mediaAreaBounds.height * 0.2), DEFAULT_VALUES.cameraDockMinHeight),
|
||||
(mediaAreaBounds.height - DEFAULT_VALUES.cameraDockMinHeight),
|
||||
);
|
||||
} else {
|
||||
cameraDockHeight = mediaAreaBounds.height;
|
||||
}
|
||||
} else {
|
||||
cameraDockHeight = min(
|
||||
max((mediaAreaBounds.height * 0.2), DEFAULT_VALUES.cameraDockMinHeight),
|
||||
max(input.cameraDock.height, DEFAULT_VALUES.cameraDockMinHeight),
|
||||
(mediaAreaBounds.height - DEFAULT_VALUES.cameraDockMinHeight),
|
||||
);
|
||||
} else {
|
||||
cameraDockHeight = mediaAreaBounds.height;
|
||||
}
|
||||
} else {
|
||||
cameraDockHeight = min(
|
||||
max(input.cameraDock.height, DEFAULT_VALUES.cameraDockMinHeight),
|
||||
(mediaAreaBounds.height - DEFAULT_VALUES.cameraDockMinHeight),
|
||||
);
|
||||
}
|
||||
|
||||
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight
|
||||
+ mediaAreaBounds.height - cameraDockHeight;
|
||||
cameraDockBounds.left = cameraDockLeft;
|
||||
cameraDockBounds.minWidth = mediaAreaBounds.width;
|
||||
cameraDockBounds.width = mediaAreaBounds.width;
|
||||
cameraDockBounds.maxWidth = mediaAreaBounds.width;
|
||||
cameraDockBounds.minHeight = DEFAULT_VALUES.cameraDockMinHeight;
|
||||
cameraDockBounds.height = cameraDockHeight;
|
||||
cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.8;
|
||||
break;
|
||||
case CAMERADOCK_POSITION.CONTENT_LEFT:
|
||||
if (input.cameraDock.width === 0) {
|
||||
if (input.presentation.isOpen) {
|
||||
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight
|
||||
+ mediaAreaBounds.height - cameraDockHeight;
|
||||
cameraDockBounds.left = cameraDockLeft;
|
||||
cameraDockBounds.minWidth = mediaAreaBounds.width;
|
||||
cameraDockBounds.width = mediaAreaBounds.width;
|
||||
cameraDockBounds.maxWidth = mediaAreaBounds.width;
|
||||
cameraDockBounds.minHeight = DEFAULT_VALUES.cameraDockMinHeight;
|
||||
cameraDockBounds.height = cameraDockHeight;
|
||||
cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.8;
|
||||
break;
|
||||
case CAMERADOCK_POSITION.CONTENT_LEFT:
|
||||
if (input.cameraDock.width === 0) {
|
||||
if (input.presentation.isOpen) {
|
||||
cameraDockWidth = min(
|
||||
max((mediaAreaBounds.width * 0.2), DEFAULT_VALUES.cameraDockMinWidth),
|
||||
(mediaAreaBounds.width - DEFAULT_VALUES.cameraDockMinWidth),
|
||||
);
|
||||
} else {
|
||||
cameraDockWidth = mediaAreaBounds.width;
|
||||
}
|
||||
} else {
|
||||
cameraDockWidth = min(
|
||||
max((mediaAreaBounds.width * 0.2), DEFAULT_VALUES.cameraDockMinWidth),
|
||||
max(input.cameraDock.width, DEFAULT_VALUES.cameraDockMinWidth),
|
||||
(mediaAreaBounds.width - DEFAULT_VALUES.cameraDockMinWidth),
|
||||
);
|
||||
} else {
|
||||
cameraDockWidth = mediaAreaBounds.width;
|
||||
}
|
||||
} else {
|
||||
cameraDockWidth = min(
|
||||
max(input.cameraDock.width, DEFAULT_VALUES.cameraDockMinWidth),
|
||||
(mediaAreaBounds.width - DEFAULT_VALUES.cameraDockMinWidth),
|
||||
);
|
||||
}
|
||||
|
||||
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight;
|
||||
cameraDockBounds.left = mediaAreaBounds.left;
|
||||
cameraDockBounds.minWidth = DEFAULT_VALUES.cameraDockMinWidth;
|
||||
cameraDockBounds.width = cameraDockWidth;
|
||||
cameraDockBounds.maxWidth = mediaAreaBounds.width * 0.8;
|
||||
cameraDockBounds.minHeight = mediaAreaBounds.height;
|
||||
cameraDockBounds.height = mediaAreaBounds.height;
|
||||
cameraDockBounds.maxHeight = mediaAreaBounds.height;
|
||||
break;
|
||||
case CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM:
|
||||
if (input.cameraDock.height === 0) {
|
||||
cameraDockHeight = min(
|
||||
max((this.mainHeight() * 0.2), DEFAULT_VALUES.cameraDockMinHeight),
|
||||
(this.mainHeight() - DEFAULT_VALUES.cameraDockMinHeight),
|
||||
);
|
||||
} else {
|
||||
cameraDockHeight = min(
|
||||
max(input.cameraDock.height, DEFAULT_VALUES.cameraDockMinHeight),
|
||||
(this.mainHeight() - DEFAULT_VALUES.cameraDockMinHeight),
|
||||
);
|
||||
}
|
||||
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight;
|
||||
cameraDockBounds.left = mediaAreaBounds.left;
|
||||
cameraDockBounds.minWidth = DEFAULT_VALUES.cameraDockMinWidth;
|
||||
cameraDockBounds.width = cameraDockWidth;
|
||||
cameraDockBounds.maxWidth = mediaAreaBounds.width * 0.8;
|
||||
cameraDockBounds.minHeight = mediaAreaBounds.height;
|
||||
cameraDockBounds.height = mediaAreaBounds.height;
|
||||
cameraDockBounds.maxHeight = mediaAreaBounds.height;
|
||||
break;
|
||||
case CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM:
|
||||
if (input.cameraDock.height === 0) {
|
||||
cameraDockHeight = min(
|
||||
max((this.mainHeight() * 0.2), DEFAULT_VALUES.cameraDockMinHeight),
|
||||
(this.mainHeight() - DEFAULT_VALUES.cameraDockMinHeight),
|
||||
);
|
||||
} else {
|
||||
cameraDockHeight = min(
|
||||
max(input.cameraDock.height, DEFAULT_VALUES.cameraDockMinHeight),
|
||||
(this.mainHeight() - DEFAULT_VALUES.cameraDockMinHeight),
|
||||
);
|
||||
}
|
||||
|
||||
cameraDockBounds.top = this.mainHeight() - cameraDockHeight;
|
||||
cameraDockBounds.left = sidebarNavWidth;
|
||||
cameraDockBounds.minWidth = sidebarContentWidth;
|
||||
cameraDockBounds.width = sidebarContentWidth;
|
||||
cameraDockBounds.maxWidth = sidebarContentWidth;
|
||||
cameraDockBounds.minHeight = DEFAULT_VALUES.cameraDockMinHeight;
|
||||
cameraDockBounds.height = cameraDockHeight;
|
||||
cameraDockBounds.maxHeight = this.mainHeight() * 0.8;
|
||||
break;
|
||||
default:
|
||||
console.log('default');
|
||||
cameraDockBounds.top = this.mainHeight() - cameraDockHeight;
|
||||
cameraDockBounds.left = sidebarNavWidth;
|
||||
cameraDockBounds.minWidth = sidebarContentWidth;
|
||||
cameraDockBounds.width = sidebarContentWidth;
|
||||
cameraDockBounds.maxWidth = sidebarContentWidth;
|
||||
cameraDockBounds.minHeight = DEFAULT_VALUES.cameraDockMinHeight;
|
||||
cameraDockBounds.height = cameraDockHeight;
|
||||
cameraDockBounds.maxHeight = this.mainHeight() * 0.8;
|
||||
break;
|
||||
default:
|
||||
console.log('default');
|
||||
}
|
||||
|
||||
if (fullscreen.group === 'webcams') {
|
||||
cameraDockBounds.width = windowWidth();
|
||||
cameraDockBounds.minWidth = windowWidth();
|
||||
cameraDockBounds.maxWidth = windowWidth();
|
||||
cameraDockBounds.height = windowHeight();
|
||||
cameraDockBounds.minHeight = windowHeight();
|
||||
cameraDockBounds.maxHeight = windowHeight();
|
||||
cameraDockBounds.top = 0;
|
||||
cameraDockBounds.left = 0;
|
||||
cameraDockBounds.zIndex = 99;
|
||||
return cameraDockBounds;
|
||||
}
|
||||
|
||||
if (input.cameraDock.isDragging) cameraDockBounds.zIndex = 99;
|
||||
else cameraDockBounds.zIndex = 1;
|
||||
}
|
||||
|
||||
if (fullscreen.group === 'webcams') {
|
||||
cameraDockBounds.width = windowWidth();
|
||||
cameraDockBounds.minWidth = windowWidth();
|
||||
cameraDockBounds.maxWidth = windowWidth();
|
||||
cameraDockBounds.height = windowHeight();
|
||||
cameraDockBounds.minHeight = windowHeight();
|
||||
cameraDockBounds.maxHeight = windowHeight();
|
||||
cameraDockBounds.top = 0;
|
||||
cameraDockBounds.left = 0;
|
||||
cameraDockBounds.zIndex = 99;
|
||||
return cameraDockBounds;
|
||||
}
|
||||
|
||||
if (input.cameraDock.isDragging) cameraDockBounds.zIndex = 99;
|
||||
else cameraDockBounds.zIndex = 1;
|
||||
} else {
|
||||
cameraDockBounds.width = 0;
|
||||
cameraDockBounds.height = 0;
|
||||
@ -590,12 +605,23 @@ class CustomLayout extends Component {
|
||||
calculatesMediaBounds(sidebarNavWidth, sidebarContentWidth, cameraDockBounds) {
|
||||
const { newLayoutContextState } = this.props;
|
||||
const { input, fullscreen } = newLayoutContextState;
|
||||
const { presentation } = input;
|
||||
const { isOpen } = presentation;
|
||||
const mediaAreaHeight = this.mainHeight()
|
||||
- (DEFAULT_VALUES.navBarHeight + DEFAULT_VALUES.actionBarHeight);
|
||||
const mediaAreaWidth = this.mainWidth() - (sidebarNavWidth + sidebarContentWidth);
|
||||
const mediaBounds = {};
|
||||
const { element: fullscreenElement } = fullscreen;
|
||||
|
||||
if (!isOpen) {
|
||||
mediaBounds.width = 0;
|
||||
mediaBounds.height = 0;
|
||||
mediaBounds.top = 0;
|
||||
mediaBounds.left = 0;
|
||||
mediaBounds.zIndex = 0;
|
||||
return mediaBounds;
|
||||
}
|
||||
|
||||
if (fullscreenElement === 'Presentation' || fullscreenElement === 'Screenshare') {
|
||||
mediaBounds.width = this.mainWidth();
|
||||
mediaBounds.height = this.mainHeight();
|
||||
|
@ -3,8 +3,10 @@ import { throttle, defaultsDeep } from 'lodash';
|
||||
import NewLayoutContext from '../context/context';
|
||||
import DEFAULT_VALUES from '../defaultValues';
|
||||
import { INITIAL_INPUT_STATE } from '../context/initState';
|
||||
import { DEVICE_TYPE, ACTIONS } from '../enums';
|
||||
import { DEVICE_TYPE, ACTIONS, PANELS } from '../enums';
|
||||
|
||||
const windowWidth = () => window.document.documentElement.clientWidth;
|
||||
const windowHeight = () => window.document.documentElement.clientHeight;
|
||||
const min = (value1, value2) => (value1 <= value2 ? value1 : value2);
|
||||
const max = (value1, value2) => (value1 >= value2 ? value1 : value2);
|
||||
|
||||
@ -35,7 +37,8 @@ class PresentationFocusLayout extends Component {
|
||||
return newLayoutContextState.input !== nextProps.newLayoutContextState.input
|
||||
|| newLayoutContextState.deviceType !== nextProps.newLayoutContextState.deviceType
|
||||
|| newLayoutContextState.layoutLoaded !== nextProps.newLayoutContextState.layoutLoaded
|
||||
|| newLayoutContextState.fontSize !== nextProps.newLayoutContextState.fontSize;
|
||||
|| newLayoutContextState.fontSize !== nextProps.newLayoutContextState.fontSize
|
||||
|| newLayoutContextState.fullscreen !== nextProps.newLayoutContextState.fullscreen;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@ -107,6 +110,8 @@ class PresentationFocusLayout extends Component {
|
||||
}, INITIAL_INPUT_STATE),
|
||||
});
|
||||
} else {
|
||||
const { sidebarContentPanel } = input.sidebarContent;
|
||||
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_LAYOUT_INPUT,
|
||||
value: defaultsDeep({
|
||||
@ -114,8 +119,10 @@ class PresentationFocusLayout extends Component {
|
||||
isOpen: true,
|
||||
},
|
||||
sidebarContent: {
|
||||
isOpen: deviceType === DEVICE_TYPE.TABLET_LANDSCAPE
|
||||
|| deviceType === DEVICE_TYPE.DESKTOP,
|
||||
isOpen: sidebarContentPanel !== PANELS.NONE
|
||||
&& (deviceType === DEVICE_TYPE.TABLET_LANDSCAPE
|
||||
|| deviceType === DEVICE_TYPE.DESKTOP),
|
||||
sidebarContentPanel,
|
||||
},
|
||||
SidebarContentHorizontalResizer: {
|
||||
isOpen: false,
|
||||
@ -234,7 +241,7 @@ class PresentationFocusLayout extends Component {
|
||||
return {
|
||||
top,
|
||||
left: sidebarNavLeft,
|
||||
zIndex: deviceType === DEVICE_TYPE.MOBILE ? 11 : 1,
|
||||
zIndex: deviceType === DEVICE_TYPE.MOBILE ? 11 : 2,
|
||||
};
|
||||
}
|
||||
|
||||
@ -305,7 +312,7 @@ class PresentationFocusLayout extends Component {
|
||||
top,
|
||||
left: deviceType === DEVICE_TYPE.MOBILE
|
||||
|| deviceType === DEVICE_TYPE.TABLET_PORTRAIT ? 0 : sidebarNavWidth,
|
||||
zIndex: deviceType === DEVICE_TYPE.MOBILE ? 11 : 2,
|
||||
zIndex: deviceType === DEVICE_TYPE.MOBILE ? 11 : 1,
|
||||
};
|
||||
}
|
||||
|
||||
@ -351,11 +358,25 @@ class PresentationFocusLayout extends Component {
|
||||
sidebarContentWidth,
|
||||
) {
|
||||
const { newLayoutContextState } = this.props;
|
||||
const { deviceType, input } = newLayoutContextState;
|
||||
const { deviceType, input, fullscreen } = newLayoutContextState;
|
||||
const cameraDockBounds = {};
|
||||
|
||||
if (input.cameraDock.numCameras > 0) {
|
||||
let cameraDockHeight = 0;
|
||||
|
||||
if (fullscreen.group === 'webcams') {
|
||||
cameraDockBounds.width = windowWidth();
|
||||
cameraDockBounds.minWidth = windowWidth();
|
||||
cameraDockBounds.maxWidth = windowWidth();
|
||||
cameraDockBounds.height = windowHeight();
|
||||
cameraDockBounds.minHeight = windowHeight();
|
||||
cameraDockBounds.maxHeight = windowHeight();
|
||||
cameraDockBounds.top = 0;
|
||||
cameraDockBounds.left = 0;
|
||||
cameraDockBounds.zIndex = 99;
|
||||
return cameraDockBounds;
|
||||
}
|
||||
|
||||
if (deviceType === DEVICE_TYPE.MOBILE) {
|
||||
cameraDockBounds.top = mediaAreaBounds.top + mediaBounds.height;
|
||||
cameraDockBounds.left = mediaAreaBounds.left;
|
||||
|
@ -3,7 +3,7 @@ import _ from 'lodash';
|
||||
import NewLayoutContext from '../context/context';
|
||||
import DEFAULT_VALUES from '../defaultValues';
|
||||
import { INITIAL_INPUT_STATE } from '../context/initState';
|
||||
import { DEVICE_TYPE, ACTIONS } from '../enums';
|
||||
import { DEVICE_TYPE, ACTIONS, PANELS } from '../enums';
|
||||
|
||||
const windowWidth = () => window.document.documentElement.clientWidth;
|
||||
const windowHeight = () => window.document.documentElement.clientHeight;
|
||||
@ -111,6 +111,8 @@ class SmartLayout extends Component {
|
||||
}, INITIAL_INPUT_STATE),
|
||||
});
|
||||
} else {
|
||||
const { sidebarContentPanel } = input.sidebarContent;
|
||||
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_LAYOUT_INPUT,
|
||||
value: _.defaultsDeep({
|
||||
@ -118,8 +120,10 @@ class SmartLayout extends Component {
|
||||
isOpen: true,
|
||||
},
|
||||
sidebarContent: {
|
||||
isOpen: deviceType === DEVICE_TYPE.TABLET_LANDSCAPE
|
||||
|| deviceType === DEVICE_TYPE.DESKTOP,
|
||||
isOpen: sidebarContentPanel !== PANELS.NONE
|
||||
&& (deviceType === DEVICE_TYPE.TABLET_LANDSCAPE
|
||||
|| deviceType === DEVICE_TYPE.DESKTOP),
|
||||
sidebarContentPanel,
|
||||
},
|
||||
SidebarContentHorizontalResizer: {
|
||||
isOpen: false,
|
||||
@ -238,7 +242,7 @@ class SmartLayout extends Component {
|
||||
return {
|
||||
top,
|
||||
left: sidebarNavLeft,
|
||||
zIndex: deviceType === DEVICE_TYPE.MOBILE ? 11 : 1,
|
||||
zIndex: deviceType === DEVICE_TYPE.MOBILE ? 11 : 2,
|
||||
};
|
||||
}
|
||||
|
||||
@ -307,7 +311,7 @@ class SmartLayout extends Component {
|
||||
top,
|
||||
left: deviceType === DEVICE_TYPE.MOBILE
|
||||
|| deviceType === DEVICE_TYPE.TABLET_PORTRAIT ? 0 : sidebarNavWidth,
|
||||
zIndex: deviceType === DEVICE_TYPE.MOBILE ? 11 : 2,
|
||||
zIndex: deviceType === DEVICE_TYPE.MOBILE ? 11 : 1,
|
||||
};
|
||||
}
|
||||
|
||||
@ -349,6 +353,8 @@ class SmartLayout extends Component {
|
||||
calculatesCameraDockBounds(mediaAreaBounds, mediaBounds) {
|
||||
const { newLayoutContextState } = this.props;
|
||||
const { input, fullscreen } = newLayoutContextState;
|
||||
const { presentation } = input;
|
||||
const { isOpen } = presentation;
|
||||
|
||||
const cameraDockBounds = {};
|
||||
|
||||
@ -357,7 +363,12 @@ class SmartLayout extends Component {
|
||||
cameraDockBounds.left = mediaAreaBounds.left;
|
||||
cameraDockBounds.zIndex = 1;
|
||||
|
||||
if (mediaBounds.width < mediaAreaBounds.width) {
|
||||
if (!isOpen) {
|
||||
cameraDockBounds.width = mediaAreaBounds.width;
|
||||
cameraDockBounds.maxWidth = mediaAreaBounds.width;
|
||||
cameraDockBounds.height = mediaAreaBounds.height;
|
||||
cameraDockBounds.maxHeight = mediaAreaBounds.height;
|
||||
} else if (mediaBounds.width < mediaAreaBounds.width) {
|
||||
cameraDockBounds.width = mediaAreaBounds.width - mediaBounds.width;
|
||||
cameraDockBounds.maxWidth = mediaAreaBounds.width * 0.8;
|
||||
cameraDockBounds.height = mediaAreaBounds.height;
|
||||
@ -424,11 +435,22 @@ class SmartLayout extends Component {
|
||||
calculatesMediaBounds(mediaAreaBounds, slideSize) {
|
||||
const { newLayoutContextState } = this.props;
|
||||
const { input, fullscreen } = newLayoutContextState;
|
||||
const { presentation } = input;
|
||||
const { isOpen } = presentation;
|
||||
const mediaBounds = {};
|
||||
const { element: fullscreenElement } = fullscreen;
|
||||
|
||||
// TODO Adicionar min e max para a apresentação
|
||||
|
||||
if (!isOpen) {
|
||||
mediaBounds.width = 0;
|
||||
mediaBounds.height = 0;
|
||||
mediaBounds.top = 0;
|
||||
mediaBounds.left = 0;
|
||||
mediaBounds.zIndex = 0;
|
||||
return mediaBounds;
|
||||
}
|
||||
|
||||
if (fullscreenElement === 'Presentation' || fullscreenElement === 'Screenshare') {
|
||||
mediaBounds.width = this.mainWidth();
|
||||
mediaBounds.height = this.mainHeight();
|
||||
|
@ -3,7 +3,7 @@ import { throttle, defaultsDeep } from 'lodash';
|
||||
import LayoutContext from '../context/context';
|
||||
import DEFAULT_VALUES from '../defaultValues';
|
||||
import { INITIAL_INPUT_STATE } from '../context/initState';
|
||||
import { DEVICE_TYPE, ACTIONS } from '../enums';
|
||||
import { DEVICE_TYPE, ACTIONS, PANELS } from '../enums';
|
||||
|
||||
const windowWidth = () => window.document.documentElement.clientWidth;
|
||||
const windowHeight = () => window.document.documentElement.clientHeight;
|
||||
@ -114,6 +114,8 @@ class VideoFocusLayout extends Component {
|
||||
),
|
||||
});
|
||||
} else {
|
||||
const { sidebarContentPanel } = input.sidebarContent;
|
||||
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_LAYOUT_INPUT,
|
||||
value: defaultsDeep(
|
||||
@ -122,8 +124,10 @@ class VideoFocusLayout extends Component {
|
||||
isOpen: true,
|
||||
},
|
||||
sidebarContent: {
|
||||
isOpen: deviceType === DEVICE_TYPE.TABLET_LANDSCAPE
|
||||
|| deviceType === DEVICE_TYPE.DESKTOP,
|
||||
isOpen: sidebarContentPanel !== PANELS.NONE
|
||||
&& (deviceType === DEVICE_TYPE.TABLET_LANDSCAPE
|
||||
|| deviceType === DEVICE_TYPE.DESKTOP),
|
||||
sidebarContentPanel,
|
||||
},
|
||||
SidebarContentHorizontalResizer: {
|
||||
isOpen: false,
|
||||
|
@ -16,6 +16,7 @@ const propTypes = {
|
||||
swapLayout: PropTypes.bool,
|
||||
audioModalIsOpen: PropTypes.bool,
|
||||
layoutContextState: PropTypes.instanceOf(Object).isRequired,
|
||||
isRTL: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
@ -42,6 +43,7 @@ export default class Media extends Component {
|
||||
usersVideo,
|
||||
layoutContextState,
|
||||
isMeteorConnected,
|
||||
isRTL,
|
||||
} = this.props;
|
||||
|
||||
const { webcamsPlacement: placement } = layoutContextState;
|
||||
@ -112,6 +114,7 @@ export default class Media extends Component {
|
||||
hideOverlay={hideOverlay}
|
||||
audioModalIsOpen={audioModalIsOpen}
|
||||
usersVideo={usersVideo}
|
||||
isRTL={isRTL}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -1,10 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Session } from 'meteor/session';
|
||||
import { notify } from '/imports/ui/services/notification';
|
||||
import VideoService from '/imports/ui/components/video-provider/service';
|
||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import { withModalMounter } from '/imports/ui/components/modal/service';
|
||||
@ -23,41 +21,9 @@ const LAYOUT_CONFIG = Meteor.settings.public.layout;
|
||||
|
||||
const propTypes = {
|
||||
isScreensharing: PropTypes.bool.isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
screenshareStarted: {
|
||||
id: 'app.media.screenshare.start',
|
||||
description: 'toast to show when a screenshare has started',
|
||||
},
|
||||
screenshareEnded: {
|
||||
id: 'app.media.screenshare.end',
|
||||
description: 'toast to show when a screenshare has ended',
|
||||
},
|
||||
});
|
||||
|
||||
class MediaContainer extends Component {
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isScreensharing,
|
||||
intl,
|
||||
} = this.props;
|
||||
const {
|
||||
isScreensharing: wasScreenSharing,
|
||||
} = prevProps;
|
||||
|
||||
if (isScreensharing !== wasScreenSharing) {
|
||||
if (!wasScreenSharing) {
|
||||
notify(intl.formatMessage(intlMessages.screenshareStarted), 'info', 'desktop');
|
||||
} else {
|
||||
notify(intl.formatMessage(intlMessages.screenshareEnded), 'info', 'desktop');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Media {...this.props} />;
|
||||
}
|
||||
@ -138,7 +104,8 @@ export default withLayoutConsumer(withModalMounter(withTracker(() => {
|
||||
}
|
||||
|
||||
data.webcamsPlacement = Storage.getItem('webcamsPlacement');
|
||||
data.isRTL = document.documentElement.getAttribute('dir') === 'rtl';
|
||||
|
||||
MediaContainer.propTypes = propTypes;
|
||||
return data;
|
||||
})(injectIntl(MediaContainer))));
|
||||
})(MediaContainer)));
|
||||
|
@ -5,6 +5,7 @@ import Auth from '/imports/ui/services/auth';
|
||||
import Users from '/imports/api/users';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import { ACTIONS } from '../layout/enums';
|
||||
|
||||
const LAYOUT_CONFIG = Meteor.settings.public.layout;
|
||||
const KURENTO_CONFIG = Meteor.settings.public.kurento;
|
||||
@ -51,10 +52,15 @@ const setSwapLayout = () => {
|
||||
swapLayout.tracker.changed();
|
||||
};
|
||||
|
||||
const toggleSwapLayout = () => {
|
||||
const toggleSwapLayout = (newLayoutContextDispatch) => {
|
||||
window.dispatchEvent(new Event('togglePresentationHide'));
|
||||
swapLayout.value = !swapLayout.value;
|
||||
swapLayout.tracker.changed();
|
||||
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: !swapLayout.value,
|
||||
});
|
||||
};
|
||||
|
||||
export const shouldEnableSwapLayout = () => !shouldShowScreenshare() && !shouldShowExternalVideo();
|
||||
|
@ -20,6 +20,7 @@ const propTypes = {
|
||||
refMediaContainer: PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
layoutContextState: PropTypes.objectOf(Object).isRequired,
|
||||
layoutContextDispatch: PropTypes.func.isRequired,
|
||||
isRTL: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
@ -196,12 +197,12 @@ class WebcamDraggable extends PureComponent {
|
||||
}
|
||||
|
||||
calculatePosition() {
|
||||
const { layoutContextState } = this.props;
|
||||
const { layoutContextState, isRTL } = this.props;
|
||||
const { mediaBounds } = layoutContextState;
|
||||
|
||||
const { top: mediaTop, left: mediaLeft } = mediaBounds;
|
||||
const { top: webcamsListTop, left: webcamsListLeft } = this.getWebcamsListBounds();
|
||||
const x = webcamsListLeft - mediaLeft;
|
||||
const x = !isRTL ? (webcamsListLeft - mediaLeft) : webcamsListLeft;
|
||||
const y = webcamsListTop - mediaTop;
|
||||
return {
|
||||
x,
|
||||
@ -279,6 +280,7 @@ class WebcamDraggable extends PureComponent {
|
||||
swapLayout,
|
||||
hideOverlay,
|
||||
audioModalIsOpen,
|
||||
isRTL,
|
||||
} = this.props;
|
||||
|
||||
const { isMobile } = deviceInfo;
|
||||
@ -426,7 +428,7 @@ class WebcamDraggable extends PureComponent {
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={dropZoneLeftClassName}
|
||||
className={!isRTL ? dropZoneLeftClassName : dropZoneRightClassName}
|
||||
style={{
|
||||
width: '15vh',
|
||||
height: `calc(${mediaHeight}px - (15vh * 2))`,
|
||||
@ -510,7 +512,7 @@ class WebcamDraggable extends PureComponent {
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={dropZoneRightClassName}
|
||||
className={!isRTL ? dropZoneRightClassName : dropZoneLeftClassName}
|
||||
style={{
|
||||
width: '15vh',
|
||||
height: `calc(${mediaHeight}px - (15vh * 2))`,
|
||||
|
@ -41,6 +41,14 @@ const intlMessage = defineMessages({
|
||||
id: 'app.meeting.endedByUserMessage',
|
||||
description: 'message informing who ended the meeting',
|
||||
},
|
||||
messageEndedByNoModeratorSingular: {
|
||||
id: 'app.meeting.endedByNoModeratorMessageSingular',
|
||||
description: 'message informing that the meeting was ended due to no moderator present (singular)',
|
||||
},
|
||||
messageEndedByNoModeratorPlural: {
|
||||
id: 'app.meeting.endedByNoModeratorMessagePlural',
|
||||
description: 'message informing that the meeting was ended due to no moderator present (plural)',
|
||||
},
|
||||
buttonOkay: {
|
||||
id: 'app.meeting.endNotification.ok.label',
|
||||
description: 'label okay for button',
|
||||
@ -100,11 +108,13 @@ const propTypes = {
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
code: PropTypes.string.isRequired,
|
||||
reason: PropTypes.string,
|
||||
ejectedReason: PropTypes.string,
|
||||
endedReason: PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
reason: null,
|
||||
ejectedReason: null,
|
||||
endedReason: null,
|
||||
};
|
||||
|
||||
class MeetingEnded extends PureComponent {
|
||||
@ -128,6 +138,8 @@ class MeetingEnded extends PureComponent {
|
||||
|
||||
const meeting = Meetings.findOne({ id: user.meetingID });
|
||||
if (meeting) {
|
||||
this.endWhenNoModeratorMinutes = meeting.durationProps.endWhenNoModeratorDelayInMinutes;
|
||||
|
||||
const endedBy = Users.findOne({
|
||||
userId: meeting.meetingEndedBy,
|
||||
}, { fields: { name: 1 } });
|
||||
@ -141,6 +153,7 @@ class MeetingEnded extends PureComponent {
|
||||
this.confirmRedirect = this.confirmRedirect.bind(this);
|
||||
this.sendFeedback = this.sendFeedback.bind(this);
|
||||
this.shouldShowFeedback = this.shouldShowFeedback.bind(this);
|
||||
this.getEndingMessage = this.getEndingMessage.bind(this);
|
||||
|
||||
AudioManager.exitAudio();
|
||||
Meteor.disconnect();
|
||||
@ -168,6 +181,26 @@ class MeetingEnded extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
getEndingMessage() {
|
||||
const { intl, code, endedReason } = this.props;
|
||||
|
||||
if (endedReason && endedReason === 'ENDED_DUE_TO_NO_MODERATOR') {
|
||||
return this.endWhenNoModeratorMinutes === 1
|
||||
? intl.formatMessage(intlMessage.messageEndedByNoModeratorSingular)
|
||||
: intl.formatMessage(intlMessage.messageEndedByNoModeratorPlural, { 0: this.endWhenNoModeratorMinutes });
|
||||
}
|
||||
|
||||
if (this.meetingEndedBy) {
|
||||
return intl.formatMessage(intlMessage.messageEndedByUser, { 0: this.meetingEndedBy });
|
||||
}
|
||||
|
||||
if (intlMessage[code]) {
|
||||
return intl.formatMessage(intlMessage[code]);
|
||||
}
|
||||
|
||||
return intl.formatMessage(intlMessage[430]);
|
||||
}
|
||||
|
||||
sendFeedback() {
|
||||
const {
|
||||
selected,
|
||||
@ -215,19 +248,17 @@ class MeetingEnded extends PureComponent {
|
||||
}
|
||||
|
||||
renderNoFeedback() {
|
||||
const { intl, code, reason } = this.props;
|
||||
const { intl, code, ejectedReason } = this.props;
|
||||
|
||||
const logMessage = reason === 'user_requested_eject_reason' ? 'User removed from the meeting' : 'Meeting ended component, no feedback configured';
|
||||
logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code, reason } }, logMessage);
|
||||
const logMessage = ejectedReason === 'user_requested_eject_reason' ? 'User removed from the meeting' : 'Meeting ended component, no feedback configured';
|
||||
logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code, reason: ejectedReason } }, logMessage);
|
||||
|
||||
return (
|
||||
<div className={styles.parent}>
|
||||
<div className={styles.modal}>
|
||||
<div className={styles.content}>
|
||||
<h1 className={styles.title} data-test="meetingEndedModalTitle">
|
||||
{this.meetingEndedBy
|
||||
? intl.formatMessage(intlMessage.messageEndedByUser, { 0: this.meetingEndedBy })
|
||||
: intl.formatMessage(intlMessage[code] || intlMessage[430])}
|
||||
{this.getEndingMessage()}
|
||||
</h1>
|
||||
{!allowRedirectToLogoutURL() ? null : (
|
||||
<div>
|
||||
@ -268,7 +299,7 @@ class MeetingEnded extends PureComponent {
|
||||
}
|
||||
|
||||
renderFeedback() {
|
||||
const { intl, code, reason } = this.props;
|
||||
const { intl, code, ejectedReason } = this.props;
|
||||
const {
|
||||
selected,
|
||||
dispatched,
|
||||
@ -276,17 +307,15 @@ class MeetingEnded extends PureComponent {
|
||||
|
||||
const noRating = selected <= 0;
|
||||
|
||||
const logMessage = reason === 'user_requested_eject_reason' ? 'User removed from the meeting' : 'Meeting ended component, feedback allowed';
|
||||
logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code, reason } }, logMessage);
|
||||
const logMessage = ejectedReason === 'user_requested_eject_reason' ? 'User removed from the meeting' : 'Meeting ended component, feedback allowed';
|
||||
logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code, reason: ejectedReason } }, logMessage);
|
||||
|
||||
return (
|
||||
<div className={styles.parent}>
|
||||
<div className={styles.modal} data-test="meetingEndedModal">
|
||||
<div className={styles.content}>
|
||||
<h1 className={styles.title}>
|
||||
{
|
||||
intl.formatMessage(intlMessage[reason] || intlMessage[430])
|
||||
}
|
||||
{this.getEndingMessage()}
|
||||
</h1>
|
||||
<div className={styles.text}>
|
||||
{this.shouldShowFeedback()
|
||||
|
151
bigbluebutton-html5/imports/ui/components/menu/component.jsx
Normal file
151
bigbluebutton-html5/imports/ui/components/menu/component.jsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { defineMessages, injectIntl } from "react-intl";
|
||||
|
||||
import Menu from "@material-ui/core/Menu";
|
||||
import MenuItem from "@material-ui/core/MenuItem";
|
||||
import { Divider } from "@material-ui/core";
|
||||
|
||||
import Icon from "/imports/ui/components/icon/component";
|
||||
import Button from "/imports/ui/components/button/component";
|
||||
|
||||
import { styles } from "./styles";
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
close: {
|
||||
id: 'app.dropdown.close',
|
||||
description: 'Close button label',
|
||||
},
|
||||
});
|
||||
|
||||
//Used to switch to mobile view
|
||||
const MAX_WIDTH = 640;
|
||||
|
||||
class BBBMenu extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
anchorEl: null,
|
||||
};
|
||||
|
||||
this.setAnchorEl = this.setAnchorEl.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
}
|
||||
|
||||
handleClick(event) {
|
||||
this.setState({ anchorEl: event.currentTarget})
|
||||
};
|
||||
|
||||
handleClose() {
|
||||
const { onCloseCallback } = this.props;
|
||||
this.setState({ anchorEl: null}, onCloseCallback());
|
||||
};
|
||||
|
||||
setAnchorEl(el) {
|
||||
this.setState({ anchorEl: el });
|
||||
};
|
||||
|
||||
makeMenuItems() {
|
||||
const { actions, selectedEmoji } = this.props;
|
||||
|
||||
return actions?.map(a => {
|
||||
const { label, onClick, key } = a;
|
||||
const itemClasses = [styles.menuitem];
|
||||
|
||||
if (key?.toLowerCase()?.includes(selectedEmoji?.toLowerCase())) itemClasses.push(styles.emojiSelected);
|
||||
|
||||
return [<MenuItem
|
||||
key={label}
|
||||
className={itemClasses.join(' ')}
|
||||
disableRipple={true}
|
||||
disableGutters={true}
|
||||
style={{ paddingLeft: '4px',paddingRight: '4px',paddingTop: '8px', paddingBottom: '8px', marginLeft: '4px', marginRight: '4px' }}
|
||||
onClick={() => {
|
||||
onClick();
|
||||
const close = !label.includes('Set status') && !label.includes('Back');
|
||||
// prevent menu close for sub menu actions
|
||||
if (close) this.handleClose();
|
||||
}}>
|
||||
<div style={{ display: 'flex', flexFlow: 'row', width: '100%'}}>
|
||||
{a.icon ? <Icon iconName={a.icon} key="icon" /> : null}
|
||||
<div className={styles.option}>{label}</div>
|
||||
{a.iconRight ? <Icon iconName={a.iconRight} key="iconRight" className={styles.iRight} /> : null}
|
||||
</div>
|
||||
</MenuItem>,
|
||||
a.divider && <Divider />
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { anchorEl } = this.state;
|
||||
const { trigger, intl, opts, wide } = this.props;
|
||||
const actionsItems = this.makeMenuItems();
|
||||
|
||||
const menuClasses = [styles.menu];
|
||||
if (wide) menuClasses.push(styles.wide)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div onClick={this.handleClick}>{trigger}</div>
|
||||
<Menu
|
||||
{...opts}
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={this.handleClose}
|
||||
className={menuClasses.join(' ')}
|
||||
>
|
||||
{actionsItems}
|
||||
{anchorEl && window.innerWidth < MAX_WIDTH &&
|
||||
<Button
|
||||
className={styles.closeBtn}
|
||||
label={intl.formatMessage(intlMessages.close)}
|
||||
size="lg"
|
||||
color="default"
|
||||
onClick={this.handleClose}
|
||||
/>
|
||||
}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(BBBMenu);
|
||||
|
||||
BBBMenu.defaultProps = {
|
||||
opts: {
|
||||
id: "default-dropdown-menu",
|
||||
keepMounted: true,
|
||||
transitionDuration: 0,
|
||||
elevation: 3,
|
||||
getContentAnchorEl: null,
|
||||
fullwidth: "true",
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
transformorigin: { vertical: 'top', horizontal: 'top' },
|
||||
},
|
||||
onCloseCallback: () => {},
|
||||
wide: false,
|
||||
};
|
||||
|
||||
BBBMenu.propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
|
||||
trigger: PropTypes.element.isRequired,
|
||||
|
||||
actions: PropTypes.arrayOf(PropTypes.shape({
|
||||
key: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
icon: PropTypes.string,
|
||||
iconRight: PropTypes.string,
|
||||
divider: PropTypes.bool,
|
||||
})).isRequired,
|
||||
|
||||
onCloseCallback: PropTypes.func,
|
||||
|
||||
wide: PropTypes.bool,
|
||||
};
|
85
bigbluebutton-html5/imports/ui/components/menu/styles.scss
Normal file
85
bigbluebutton-html5/imports/ui/components/menu/styles.scss
Normal file
@ -0,0 +1,85 @@
|
||||
|
||||
@import "/imports/ui/stylesheets/variables/breakpoints";
|
||||
@import "/imports/ui/stylesheets/variables/placeholders";
|
||||
@import '/imports/ui/stylesheets/mixins/_indicators';
|
||||
|
||||
.option {
|
||||
line-height: 1;
|
||||
margin-right: 1.65rem;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.closeBtn {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 5rem;
|
||||
background-color: var(--color-white);
|
||||
padding: 1rem;
|
||||
|
||||
border-radius: 0;
|
||||
z-index: 1011;
|
||||
font-size: calc(var(--font-size-large) * 1.1);
|
||||
box-shadow: 0 0 0 2rem var(--color-white) !important;
|
||||
border: var(--color-white) !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.iRight {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.menu {
|
||||
@include mq($small-only) {
|
||||
:global(.MuiPaper-root.MuiMenu-paper.MuiPopover-paper) {
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
bottom: 0 !important;
|
||||
right: 0 !important;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.MuiPaper-root) {
|
||||
background-color: var(--dropdown-bg);
|
||||
border-radius: var(--border-radius);
|
||||
border: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
}
|
||||
|
||||
.wide {
|
||||
:global(.MuiPaper-root) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.menuitem {
|
||||
transition: none !important;
|
||||
font-size: 90% !important;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: #FFF;
|
||||
background-color: var(--color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.emojiSelected {
|
||||
div,
|
||||
i {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
div,
|
||||
i {
|
||||
color: #FFF ;
|
||||
}
|
||||
}
|
||||
}
|
@ -39,7 +39,7 @@ const isLocked = () => {
|
||||
const meeting = Meetings.findOne({ meetingId: Auth.meetingID }, { fields: { 'lockSettingsProps.disableNote': 1 } });
|
||||
const user = Users.findOne({ userId: Auth.userID }, { fields: { locked: 1, role: 1 } });
|
||||
|
||||
if (meeting.lockSettingsProps && user.role !== ROLE_MODERATOR) {
|
||||
if (meeting.lockSettingsProps && user.role !== ROLE_MODERATOR && user.locked) {
|
||||
return meeting.lockSettingsProps.disableNote;
|
||||
}
|
||||
return false;
|
||||
|
@ -15,14 +15,8 @@ import { withLayoutConsumer } from '/imports/ui/components/layout/context';
|
||||
import {
|
||||
USERLIST_MIN_WIDTH,
|
||||
USERLIST_MAX_WIDTH,
|
||||
CHAT_MIN_WIDTH,
|
||||
CHAT_MAX_WIDTH,
|
||||
POLL_MIN_WIDTH,
|
||||
POLL_MAX_WIDTH,
|
||||
NOTE_MIN_WIDTH,
|
||||
NOTE_MAX_WIDTH,
|
||||
BREAKOUT_MIN_WIDTH,
|
||||
BREAKOUT_MAX_WIDTH,
|
||||
PANEL_MIN_WIDTH,
|
||||
PANEL_MAX_WIDTH,
|
||||
} from '/imports/ui/components/layout/layout-manager/component';
|
||||
import { PANELS } from '../layout/enums';
|
||||
|
||||
@ -228,16 +222,17 @@ class PanelManager extends Component {
|
||||
}
|
||||
|
||||
breakoutResizeStop(addvalue) {
|
||||
const { breakoutRoomWidth } = this.state;
|
||||
const { secondPanelWidth } = this.state;
|
||||
const { layoutContextDispatch } = this.props;
|
||||
|
||||
this.setBreakoutRoomWidth(breakoutRoomWidth + addvalue);
|
||||
const newSecondPanelWidth = secondPanelWidth + addvalue;
|
||||
this.setSecondPanelWidth(newSecondPanelWidth);
|
||||
|
||||
layoutContextDispatch(
|
||||
{
|
||||
type: 'setBreakoutRoomSize',
|
||||
value: {
|
||||
width: breakoutRoomWidth + addvalue,
|
||||
width: newSecondPanelWidth,
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -332,8 +327,8 @@ class PanelManager extends Component {
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
minWidth={CHAT_MIN_WIDTH}
|
||||
maxWidth={CHAT_MAX_WIDTH}
|
||||
minWidth={PANEL_MIN_WIDTH}
|
||||
maxWidth={PANEL_MAX_WIDTH}
|
||||
ref={(node) => { this.resizableChat = node; }}
|
||||
enable={resizableEnableOptions}
|
||||
key={this.chatKey}
|
||||
@ -379,8 +374,8 @@ class PanelManager extends Component {
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
minWidth={NOTE_MIN_WIDTH}
|
||||
maxWidth={NOTE_MAX_WIDTH}
|
||||
minWidth={PANEL_MIN_WIDTH}
|
||||
maxWidth={PANEL_MAX_WIDTH}
|
||||
ref={(node) => { this.resizableNote = node; }}
|
||||
enable={resizableEnableOptions}
|
||||
key={this.noteKey}
|
||||
@ -523,8 +518,8 @@ class PanelManager extends Component {
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
minWidth={BREAKOUT_MIN_WIDTH}
|
||||
maxWidth={BREAKOUT_MAX_WIDTH}
|
||||
minWidth={PANEL_MIN_WIDTH}
|
||||
maxWidth={PANEL_MAX_WIDTH}
|
||||
ref={(node) => { this.resizableBreakout = node; }}
|
||||
enable={resizableEnableOptions}
|
||||
key={this.breakoutroomKey}
|
||||
@ -563,8 +558,8 @@ class PanelManager extends Component {
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
minWidth={POLL_MIN_WIDTH}
|
||||
maxWidth={POLL_MAX_WIDTH}
|
||||
minWidth={PANEL_MIN_WIDTH}
|
||||
maxWidth={PANEL_MAX_WIDTH}
|
||||
ref={(node) => { this.resizablePoll = node; }}
|
||||
enable={resizableEnableOptions}
|
||||
key={this.pollKey}
|
||||
|
@ -21,7 +21,7 @@ import { withDraggableConsumer } from '../media/webcam-draggable-overlay/context
|
||||
import Icon from '/imports/ui/components/icon/component';
|
||||
import { withLayoutConsumer } from '/imports/ui/components/layout/context';
|
||||
import PollingContainer from '/imports/ui/components/polling/container';
|
||||
import { ACTIONS } from '../layout/enums';
|
||||
import { ACTIONS, LAYOUT_TYPE } from '../layout/enums';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
presentationLabel: {
|
||||
@ -170,6 +170,7 @@ class Presentation extends PureComponent {
|
||||
restoreOnUpdate,
|
||||
layoutContextDispatch,
|
||||
layoutContextState,
|
||||
newLayoutContextDispatch,
|
||||
userIsPresenter,
|
||||
layoutManagerLoaded,
|
||||
presentationBounds,
|
||||
@ -267,7 +268,7 @@ class Presentation extends PureComponent {
|
||||
|| slidePosition.viewBoxWidth !== prevProps.slidePosition.viewBoxWidth;
|
||||
const pollPublished = publishedPoll && !prevProps.publishedPoll;
|
||||
if (slideChanged || positionChanged || pollPublished) {
|
||||
toggleSwapLayout();
|
||||
toggleSwapLayout(newLayoutContextDispatch);
|
||||
}
|
||||
}
|
||||
|
||||
@ -492,12 +493,22 @@ class Presentation extends PureComponent {
|
||||
|
||||
renderPresentationClose() {
|
||||
const { isFullscreen } = this.state;
|
||||
const { layoutLoaded, fullscreenContext } = this.props;
|
||||
const {
|
||||
layoutLoaded,
|
||||
layoutType,
|
||||
fullscreenContext,
|
||||
newLayoutContextDispatch,
|
||||
} = this.props;
|
||||
|
||||
if (!shouldEnableSwapLayout() || isFullscreen || layoutLoaded === 'new' && fullscreenContext) {
|
||||
if (!shouldEnableSwapLayout() ||
|
||||
isFullscreen ||
|
||||
(layoutLoaded === 'new' && (fullscreenContext || layoutType === LAYOUT_TYPE.PRESENTATION_FOCUS))) {
|
||||
return null;
|
||||
}
|
||||
return <PresentationCloseButton toggleSwapLayout={MediaService.toggleSwapLayout} />;
|
||||
return <PresentationCloseButton
|
||||
toggleSwapLayout={MediaService.toggleSwapLayout}
|
||||
newLayoutContextDispatch={newLayoutContextDispatch}
|
||||
/>;
|
||||
}
|
||||
|
||||
renderOverlays(slideObj, svgDimensions, viewBoxPosition, viewBoxDimensions, physicalDimensions) {
|
||||
|
@ -20,7 +20,7 @@ const PresentationContainer = ({ presentationPodIds, mountPresentation, ...props
|
||||
const fullscreenElementId = 'Presentation';
|
||||
const newLayoutContext = useContext(NLayoutContext);
|
||||
const { newLayoutContextState, newLayoutContextDispatch } = newLayoutContext;
|
||||
const { output, layoutLoaded, fullscreen } = newLayoutContextState;
|
||||
const { output, layoutLoaded, layoutType, fullscreen } = newLayoutContextState;
|
||||
const { presentation } = output;
|
||||
const { element } = fullscreen;
|
||||
const fullscreenContext = (element === fullscreenElementId);
|
||||
@ -43,6 +43,7 @@ const PresentationContainer = ({ presentationPodIds, mountPresentation, ...props
|
||||
userIsPresenter: userIsPresenter && !layoutSwapped,
|
||||
presentationBounds: presentation,
|
||||
layoutLoaded,
|
||||
layoutType,
|
||||
fullscreenContext,
|
||||
fullscreenElementId,
|
||||
}
|
||||
|
@ -10,13 +10,13 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const ClosePresentationComponent = ({ intl, toggleSwapLayout }) => (
|
||||
const ClosePresentationComponent = ({ intl, toggleSwapLayout, newLayoutContextDispatch }) => (
|
||||
<Button
|
||||
color="primary"
|
||||
icon="minus"
|
||||
size="sm"
|
||||
data-test="hidePresentationButton"
|
||||
onClick={toggleSwapLayout}
|
||||
onClick={() => toggleSwapLayout(newLayoutContextDispatch)}
|
||||
label={intl.formatMessage(intlMessages.closePresentationLabel)}
|
||||
aria-label={intl.formatMessage(intlMessages.closePresentationLabel)}
|
||||
hideLabel
|
||||
|
42
bigbluebutton-html5/imports/ui/components/reload-button/component.js
Executable file
42
bigbluebutton-html5/imports/ui/components/reload-button/component.js
Executable file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import cx from 'classnames';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
fullscreenButton: {
|
||||
id: 'app.fullscreenButton.label',
|
||||
description: 'Fullscreen label',
|
||||
},
|
||||
});
|
||||
|
||||
import _ from 'lodash';
|
||||
import { styles } from './styles';
|
||||
|
||||
const DEBOUNCE_TIMEOUT = 5000;
|
||||
const DEBOUNCE_OPTIONS = {
|
||||
leading: true,
|
||||
trailing: false,
|
||||
};
|
||||
|
||||
const ReloadButtonComponent = ({
|
||||
handleReload,
|
||||
label,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<div className={styles.button}>
|
||||
<Button
|
||||
color="primary"
|
||||
icon="refresh"
|
||||
size="md"
|
||||
circle
|
||||
onClick={_.debounce(handleReload, DEBOUNCE_TIMEOUT, DEBOUNCE_OPTIONS)}
|
||||
label={label}
|
||||
hideLabel
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReloadButtonComponent;
|
@ -0,0 +1,6 @@
|
||||
.button {
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
@ -10,6 +10,7 @@ import AutoplayOverlay from '../media/autoplay-overlay/component';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import playAndRetry from '/imports/utils/mediaElementPlayRetry';
|
||||
import PollingContainer from '/imports/ui/components/polling/container';
|
||||
import { notify } from '/imports/ui/services/notification';
|
||||
import { withLayoutConsumer } from '/imports/ui/components/layout/context';
|
||||
import {
|
||||
SCREENSHARE_MEDIA_ELEMENT_NAME,
|
||||
@ -44,6 +45,14 @@ const intlMessages = defineMessages({
|
||||
autoplayAllowLabel: {
|
||||
id: 'app.media.screenshare.autoplayAllowLabel',
|
||||
},
|
||||
screenshareStarted: {
|
||||
id: 'app.media.screenshare.start',
|
||||
description: 'toast to show when a screenshare has started',
|
||||
},
|
||||
screenshareEnded: {
|
||||
id: 'app.media.screenshare.end',
|
||||
description: 'toast to show when a screenshare has ended',
|
||||
},
|
||||
});
|
||||
|
||||
const ALLOW_FULLSCREEN = Meteor.settings.public.app.allowFullscreen;
|
||||
@ -69,6 +78,8 @@ class ScreenshareComponent extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { intl } = this.props;
|
||||
|
||||
screenshareHasStarted();
|
||||
this.screenshareContainer.addEventListener('fullscreenchange', this.onFullscreenChange);
|
||||
// Autoplay failure handling
|
||||
@ -77,6 +88,8 @@ class ScreenshareComponent extends React.Component {
|
||||
subscribeToStreamStateChange('screenshare', this.onStreamStateChange);
|
||||
// Attaches the local stream if it exists to serve as the local presenter preview
|
||||
attachLocalPreviewStream(getMediaElement());
|
||||
|
||||
notify(intl.formatMessage(intlMessages.screenshareStarted), 'info', 'desktop');
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@ -93,13 +106,17 @@ class ScreenshareComponent extends React.Component {
|
||||
getSwapLayout,
|
||||
shouldEnableSwapLayout,
|
||||
toggleSwapLayout,
|
||||
newLayoutContextDispatch,
|
||||
intl,
|
||||
} = this.props;
|
||||
const layoutSwapped = getSwapLayout() && shouldEnableSwapLayout();
|
||||
if (layoutSwapped) toggleSwapLayout();
|
||||
if (layoutSwapped) toggleSwapLayout(newLayoutContextDispatch);
|
||||
screenshareHasEnded();
|
||||
this.screenshareContainer.removeEventListener('fullscreenchange', this.onFullscreenChange);
|
||||
window.removeEventListener('screensharePlayFailed', this.handlePlayElementFailed);
|
||||
unsubscribeFromStreamStateChange('screenshare', this.onStreamStateChange);
|
||||
|
||||
notify(intl.formatMessage(intlMessages.screenshareEnded), 'info', 'desktop');
|
||||
}
|
||||
|
||||
onStreamStateChange(event) {
|
||||
|
@ -23,12 +23,15 @@ const SUBSCRIPTIONS = [
|
||||
];
|
||||
|
||||
const EVENT_NAME = 'bbb-group-chat-messages-subscription-has-stoppped';
|
||||
const EVENT_NAME_SUBSCRIPTION_READY = 'bbb-group-chat-messages-subscriptions-ready';
|
||||
|
||||
class Subscriptions extends Component {
|
||||
componentDidUpdate() {
|
||||
const { subscriptionsReady } = this.props;
|
||||
if (subscriptionsReady) {
|
||||
Session.set('subscriptionsReady', true);
|
||||
const event = new Event(EVENT_NAME_SUBSCRIPTION_READY);
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -299,16 +299,19 @@ const getActiveChats = ({ groupChatsMessages, groupChats, users }) => {
|
||||
const isVoiceOnlyUser = (userId) => userId.toString().startsWith('v_');
|
||||
|
||||
const isMeetingLocked = (id) => {
|
||||
const meeting = Meetings.findOne({ meetingId: id }, { fields: { lockSettingsProps: 1 } });
|
||||
const meeting = Meetings.findOne({ meetingId: id },
|
||||
{ fields: { lockSettingsProps: 1, usersProp: 1 } });
|
||||
let isLocked = false;
|
||||
|
||||
if (meeting.lockSettingsProps !== undefined) {
|
||||
const lockSettings = meeting.lockSettingsProps;
|
||||
const {lockSettingsProps:lockSettings, usersProp} = meeting;
|
||||
|
||||
if (lockSettings.disableCam
|
||||
|| lockSettings.disableMic
|
||||
|| lockSettings.disablePrivateChat
|
||||
|| lockSettings.disablePublicChat) {
|
||||
|| lockSettings.disablePublicChat
|
||||
|| lockSettings.disableNote
|
||||
|| usersProp.webcamsOnlyForModerator) {
|
||||
isLocked = true;
|
||||
}
|
||||
}
|
||||
@ -538,7 +541,7 @@ const roving = (...args) => {
|
||||
if ([KEY_CODES.ARROW_RIGHT, KEY_CODES.SPACE, KEY_CODES.ENTER].includes(event.keyCode)) {
|
||||
const tether = document.activeElement.firstChild;
|
||||
const dropdownTrigger = tether.firstChild;
|
||||
dropdownTrigger.click();
|
||||
dropdownTrigger?.click();
|
||||
focusFirstDropDownItem();
|
||||
}
|
||||
};
|
||||
|
@ -40,21 +40,19 @@
|
||||
@include elementFocus(var(--list-item-bg-hover));
|
||||
@include scrollbox-vertical(var(--user-list-bg));
|
||||
|
||||
|
||||
> div {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
@extend %highContrastOutline;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
box-shadow: none;
|
||||
box-shadow: inset 1px 0 3px var(--color-primary);
|
||||
border-radius: none;
|
||||
outline-style: none;
|
||||
outline-style: transparent;
|
||||
}
|
||||
|
||||
flex-grow: 1;
|
||||
|
@ -62,6 +62,7 @@ class UserParticipants extends Component {
|
||||
this.changeState = this.changeState.bind(this);
|
||||
this.rowRenderer = this.rowRenderer.bind(this);
|
||||
this.handleClickSelectedUser = this.handleClickSelectedUser.bind(this);
|
||||
this.selectEl = this.selectEl.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -85,13 +86,19 @@ class UserParticipants extends Component {
|
||||
return !isPropsEqual || !isStateEqual;
|
||||
}
|
||||
|
||||
selectEl(el) {
|
||||
if (!el) return null;
|
||||
if (el.getAttribute('tabindex')) return el?.focus();
|
||||
this.selectEl(el?.firstChild);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { selectedUser } = this.state;
|
||||
|
||||
if (selectedUser) {
|
||||
const { firstChild } = selectedUser;
|
||||
if (!firstChild.isEqualNode(document.activeElement)) {
|
||||
firstChild.focus();
|
||||
this.selectEl(selectedUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -166,7 +173,6 @@ class UserParticipants extends Component {
|
||||
const { roving } = this.props;
|
||||
const { selectedUser, scrollArea } = this.state;
|
||||
const usersItemsRef = findDOMNode(scrollArea.firstChild);
|
||||
|
||||
roving(event, this.changeState, usersItemsRef, selectedUser);
|
||||
}
|
||||
|
||||
@ -213,6 +219,7 @@ class UserParticipants extends Component {
|
||||
: <hr className={styles.separator} />
|
||||
}
|
||||
<div
|
||||
id={'user-list-virtualized-scroll'}
|
||||
className={styles.virtulizedScrollableList}
|
||||
tabIndex={0}
|
||||
ref={(ref) => {
|
||||
@ -244,6 +251,7 @@ class UserParticipants extends Component {
|
||||
className={styles.scrollStyle}
|
||||
overscanRowCount={30}
|
||||
deferredMeasurementCache={this.cache}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
|
@ -1,18 +1,18 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import _ from 'lodash';
|
||||
import { Session } from 'meteor/session';
|
||||
import PropTypes from 'prop-types';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import cx from 'classnames'
|
||||
import UserAvatar from '/imports/ui/components/user-avatar/component';
|
||||
import Icon from '/imports/ui/components/icon/component';
|
||||
import Dropdown from '/imports/ui/components/dropdown/component';
|
||||
import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
|
||||
import { withModalMounter } from '/imports/ui/components/modal/service';
|
||||
import RemoveUserModal from '/imports/ui/components/modal/remove-user/component';
|
||||
import _ from 'lodash';
|
||||
import { Session } from 'meteor/session';
|
||||
import BBBMenu from "/imports/ui/components/menu/component";
|
||||
import { styles } from './styles';
|
||||
import UserName from '../user-name/component';
|
||||
import Service from '/imports/ui/components/user-list/service';
|
||||
import { PANELS, ACTIONS } from '../../../../../layout/enums';
|
||||
import WhiteboardService from '/imports/ui/components/whiteboard/service';
|
||||
|
||||
@ -150,10 +150,10 @@ class UserDropdown extends PureComponent {
|
||||
|
||||
this.state = {
|
||||
isActionsOpen: false,
|
||||
dropdownOffset: 0,
|
||||
dropdownDirection: 'top',
|
||||
dropdownVisible: false,
|
||||
showNestedOptions: false,
|
||||
selected: false,
|
||||
};
|
||||
|
||||
this.handleScroll = this.handleScroll.bind(this);
|
||||
@ -162,16 +162,11 @@ class UserDropdown extends PureComponent {
|
||||
this.getDropdownMenuParent = this.getDropdownMenuParent.bind(this);
|
||||
this.renderUserAvatar = this.renderUserAvatar.bind(this);
|
||||
this.resetMenuState = this.resetMenuState.bind(this);
|
||||
this.makeDropdownItem = this.makeDropdownItem.bind(this);
|
||||
|
||||
this.title = _.uniqueId('dropdown-title-');
|
||||
this.seperator = _.uniqueId('action-separator-');
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.checkDropdownDirection();
|
||||
}
|
||||
|
||||
onActionsShow() {
|
||||
Session.set('dropdownOpen', true);
|
||||
const { getScrollContainerRef } = this.props;
|
||||
@ -187,7 +182,6 @@ class UserDropdown extends PureComponent {
|
||||
this.setState({
|
||||
isActionsOpen: true,
|
||||
dropdownVisible: false,
|
||||
dropdownOffset: dropdownTrigger.offsetTop - scrollContainer.scrollTop,
|
||||
dropdownDirection: 'top',
|
||||
});
|
||||
|
||||
@ -274,48 +268,40 @@ class UserDropdown extends PureComponent {
|
||||
|
||||
if (showNestedOptions && isMeteorConnected) {
|
||||
if (allowedToChangeStatus) {
|
||||
actions.push(this.makeDropdownItem(
|
||||
'back',
|
||||
intl.formatMessage(messages.backTriggerLabel),
|
||||
() => this.setState(
|
||||
{
|
||||
showNestedOptions: false,
|
||||
isActionsOpen: true,
|
||||
}, Session.set('dropdownOpen', true),
|
||||
),
|
||||
'left_arrow',
|
||||
));
|
||||
actions.push({
|
||||
key: "back",
|
||||
label: intl.formatMessage(messages.backTriggerLabel),
|
||||
onClick: () => this.setState({ showNestedOptions: false }),
|
||||
icon: 'left_arrow',
|
||||
divider: true,
|
||||
});
|
||||
}
|
||||
|
||||
actions.push(<Dropdown.DropdownListSeparator key={_.uniqueId('list-separator-')} />);
|
||||
|
||||
const statuses = Object.keys(getEmojiList);
|
||||
statuses.map(status => actions.push(this.makeDropdownItem(
|
||||
status,
|
||||
intl.formatMessage({ id: `app.actionsBar.emojiMenu.${status}Label` }),
|
||||
() => { setEmojiStatus(user.userId, status); this.resetMenuState(); },
|
||||
getEmojiList[status],
|
||||
)));
|
||||
|
||||
statuses.forEach(s => {
|
||||
actions.push({
|
||||
key: s,
|
||||
label: intl.formatMessage({ id: `app.actionsBar.emojiMenu.${s}Label` }),
|
||||
onClick: () => {
|
||||
setEmojiStatus(user.userId, s);
|
||||
this.resetMenuState();
|
||||
this.handleClose();
|
||||
},
|
||||
icon: getEmojiList[s],
|
||||
})
|
||||
});
|
||||
return actions;
|
||||
}
|
||||
|
||||
if (allowedToChangeStatus && isMeteorConnected) {
|
||||
actions.push(this.makeDropdownItem(
|
||||
'setstatus',
|
||||
intl.formatMessage(messages.statusTriggerLabel),
|
||||
() => this.setState(
|
||||
{
|
||||
showNestedOptions: true,
|
||||
isActionsOpen: true,
|
||||
}, () => {
|
||||
Session.set('dropdownOpen', true);
|
||||
Service.focusFirstDropDownItem();
|
||||
},
|
||||
),
|
||||
'user',
|
||||
'right_arrow',
|
||||
));
|
||||
actions.push({
|
||||
key: "setstatus",
|
||||
label: intl.formatMessage(messages.statusTriggerLabel),
|
||||
onClick: () => this.setState({ showNestedOptions: true }),
|
||||
icon: 'user',
|
||||
iconRight: 'right_arrow',
|
||||
})
|
||||
}
|
||||
|
||||
const showChatOption = CHAT_ENABLED
|
||||
@ -325,10 +311,11 @@ class UserDropdown extends PureComponent {
|
||||
&& isMeteorConnected;
|
||||
|
||||
if (showChatOption) {
|
||||
actions.push(this.makeDropdownItem(
|
||||
'activeChat',
|
||||
intl.formatMessage(messages.StartPrivateChat),
|
||||
() => {
|
||||
actions.push({
|
||||
key: "activeChat",
|
||||
label: intl.formatMessage(messages.StartPrivateChat),
|
||||
onClick: () => {
|
||||
this.handleClose();
|
||||
getGroupChatPrivate(currentUser.userId, user);
|
||||
newLayoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
@ -343,110 +330,142 @@ class UserDropdown extends PureComponent {
|
||||
value: user.userId,
|
||||
});
|
||||
},
|
||||
'chat',
|
||||
));
|
||||
icon: 'chat',
|
||||
});
|
||||
}
|
||||
|
||||
if (allowedToResetStatus && user.emoji !== 'none' && isMeteorConnected) {
|
||||
actions.push(this.makeDropdownItem(
|
||||
'clearStatus',
|
||||
intl.formatMessage(messages.ClearStatusLabel),
|
||||
() => this.onActionsHide(setEmojiStatus(user.userId, 'none')),
|
||||
'clear_status',
|
||||
));
|
||||
actions.push({
|
||||
key: "clearStatus",
|
||||
label: intl.formatMessage(messages.ClearStatusLabel),
|
||||
onClick: () => {
|
||||
this.onActionsHide(setEmojiStatus(user.userId, 'none'));
|
||||
this.handleClose();
|
||||
},
|
||||
icon: 'clear_status',
|
||||
});
|
||||
}
|
||||
|
||||
if (allowedToMuteAudio && isMeteorConnected && !meetingIsBreakout) {
|
||||
actions.push(this.makeDropdownItem(
|
||||
'mute',
|
||||
intl.formatMessage(messages.MuteUserAudioLabel),
|
||||
() => this.onActionsHide(toggleVoice(user.userId)),
|
||||
'mute',
|
||||
));
|
||||
actions.push({
|
||||
key: "mute",
|
||||
label: intl.formatMessage(messages.MuteUserAudioLabel),
|
||||
onClick: () => {
|
||||
this.onActionsHide(toggleVoice(user.userId));
|
||||
this.handleClose();
|
||||
},
|
||||
icon: 'mute',
|
||||
});
|
||||
}
|
||||
|
||||
if (allowedToUnmuteAudio && !userLocks.userMic && isMeteorConnected && !meetingIsBreakout) {
|
||||
actions.push(this.makeDropdownItem(
|
||||
'unmute',
|
||||
intl.formatMessage(messages.UnmuteUserAudioLabel),
|
||||
() => this.onActionsHide(toggleVoice(user.userId)),
|
||||
'unmute',
|
||||
));
|
||||
actions.push({
|
||||
key: "unmute",
|
||||
label: intl.formatMessage(messages.UnmuteUserAudioLabel),
|
||||
onClick: () => {
|
||||
this.onActionsHide(toggleVoice(user.userId));
|
||||
this.handleClose();
|
||||
},
|
||||
icon: 'unmute',
|
||||
});
|
||||
}
|
||||
|
||||
if (allowedToChangeWhiteboardAccess && !user.presenter && isMeteorConnected) {
|
||||
const label = user.whiteboardAccess ? intl.formatMessage(messages.removeWhiteboardAccess) : intl.formatMessage(messages.giveWhiteboardAccess);
|
||||
|
||||
actions.push(this.makeDropdownItem(
|
||||
'changeWhiteboardAccess',
|
||||
label,
|
||||
() => WhiteboardService.changeWhiteboardAccess(user.userId, !user.whiteboardAccess),
|
||||
'pen_tool',
|
||||
));
|
||||
actions.push({
|
||||
key: "changeWhiteboardAccess",
|
||||
label: label,
|
||||
onClick: () => {
|
||||
WhiteboardService.changeWhiteboardAccess(user.userId, !user.whiteboardAccess);
|
||||
this.handleClose();
|
||||
},
|
||||
icon: 'pen_tool',
|
||||
});
|
||||
}
|
||||
|
||||
if (allowedToSetPresenter && isMeteorConnected) {
|
||||
actions.push(this.makeDropdownItem(
|
||||
'setPresenter',
|
||||
isMe(user.userId)
|
||||
? intl.formatMessage(messages.takePresenterLabel)
|
||||
: intl.formatMessage(messages.makePresenterLabel),
|
||||
() => this.onActionsHide(assignPresenter(user.userId)),
|
||||
'presentation',
|
||||
));
|
||||
actions.push({
|
||||
key: "setPresenter",
|
||||
label: isMe(user.userId)
|
||||
? intl.formatMessage(messages.takePresenterLabel)
|
||||
: intl.formatMessage(messages.makePresenterLabel),
|
||||
onClick: () => {
|
||||
this.onActionsHide(assignPresenter(user.userId))
|
||||
this.handleClose();
|
||||
},
|
||||
icon: 'presentation',
|
||||
});
|
||||
}
|
||||
|
||||
if (allowedToPromote && isMeteorConnected) {
|
||||
actions.push(this.makeDropdownItem(
|
||||
'promote',
|
||||
intl.formatMessage(messages.PromoteUserLabel),
|
||||
() => this.onActionsHide(changeRole(user.userId, 'MODERATOR')),
|
||||
'promote',
|
||||
));
|
||||
actions.push({
|
||||
key: "promote",
|
||||
label: intl.formatMessage(messages.PromoteUserLabel),
|
||||
onClick: () => {
|
||||
this.onActionsHide(changeRole(user.userId, 'MODERATOR'));
|
||||
this.handleClose();
|
||||
},
|
||||
icon: 'promote',
|
||||
});
|
||||
}
|
||||
|
||||
if (allowedToDemote && isMeteorConnected) {
|
||||
actions.push(this.makeDropdownItem(
|
||||
'demote',
|
||||
intl.formatMessage(messages.DemoteUserLabel),
|
||||
() => this.onActionsHide(changeRole(user.userId, 'VIEWER')),
|
||||
'user',
|
||||
));
|
||||
actions.push({
|
||||
key: "demote",
|
||||
label: intl.formatMessage(messages.DemoteUserLabel),
|
||||
onClick: () => {
|
||||
this.onActionsHide(changeRole(user.userId, 'VIEWER'));
|
||||
this.handleClose();
|
||||
},
|
||||
icon: 'user',
|
||||
});
|
||||
}
|
||||
|
||||
if (allowedToChangeUserLockStatus && isMeteorConnected) {
|
||||
const userLocked = user.locked && user.role !== ROLE_MODERATOR;
|
||||
actions.push(this.makeDropdownItem(
|
||||
'unlockUser',
|
||||
userLocked ? intl.formatMessage(messages.UnlockUserLabel, { 0: user.name })
|
||||
: intl.formatMessage(messages.LockUserLabel, { 0: user.name }),
|
||||
() => this.onActionsHide(toggleUserLock(user.userId, !userLocked)),
|
||||
userLocked ? 'unlock' : 'lock',
|
||||
));
|
||||
|
||||
actions.push({
|
||||
key: "unlockUser",
|
||||
label: userLocked ? intl.formatMessage(messages.UnlockUserLabel, { 0: user.name })
|
||||
: intl.formatMessage(messages.LockUserLabel, { 0: user.name }),
|
||||
onClick: () => {
|
||||
this.onActionsHide(toggleUserLock(user.userId, !userLocked));
|
||||
this.handleClose();
|
||||
},
|
||||
icon: userLocked ? 'unlock' : 'lock',
|
||||
});
|
||||
}
|
||||
|
||||
if (allowUserLookup && isMeteorConnected) {
|
||||
actions.push(this.makeDropdownItem(
|
||||
'directoryLookup',
|
||||
intl.formatMessage(messages.DirectoryLookupLabel),
|
||||
() => this.onActionsHide(requestUserInformation(user.extId)),
|
||||
'user',
|
||||
));
|
||||
actions.push({
|
||||
key: "directoryLookup",
|
||||
label: intl.formatMessage(messages.DirectoryLookupLabel),
|
||||
onClick: () => {
|
||||
this.onActionsHide(requestUserInformation(user.extId));
|
||||
this.handleClose();
|
||||
},
|
||||
icon: 'user',
|
||||
});
|
||||
}
|
||||
|
||||
if (allowedToRemove && isMeteorConnected) {
|
||||
actions.push(this.makeDropdownItem(
|
||||
'remove',
|
||||
intl.formatMessage(messages.RemoveUserLabel, { 0: user.name }),
|
||||
() => this.onActionsHide(mountModal(
|
||||
<RemoveUserModal
|
||||
intl={intl}
|
||||
user={user}
|
||||
onConfirm={removeUser}
|
||||
/>,
|
||||
)),
|
||||
'circle_close',
|
||||
));
|
||||
actions.push({
|
||||
key: "remove",
|
||||
label: intl.formatMessage(messages.RemoveUserLabel, { 0: user.name }),
|
||||
onClick: () => {
|
||||
this.onActionsHide(mountModal(
|
||||
<RemoveUserModal
|
||||
intl={intl}
|
||||
user={user}
|
||||
onConfirm={removeUser}
|
||||
/>,
|
||||
))
|
||||
|
||||
this.handleClose();
|
||||
},
|
||||
icon: 'circle_close',
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
@ -456,34 +475,16 @@ class UserDropdown extends PureComponent {
|
||||
return findDOMNode(this.dropdown);
|
||||
}
|
||||
|
||||
makeDropdownItem(key, label, onClick, icon = null, iconRight = null) {
|
||||
const { getEmoji } = this.props;
|
||||
return (
|
||||
<Dropdown.DropdownListItem
|
||||
{...{
|
||||
key,
|
||||
label,
|
||||
onClick,
|
||||
icon,
|
||||
iconRight,
|
||||
}}
|
||||
className={key === getEmoji ? styles.emojiSelected : null}
|
||||
data-test={key}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
resetMenuState() {
|
||||
return this.setState({
|
||||
isActionsOpen: false,
|
||||
dropdownOffset: 0,
|
||||
dropdownDirection: 'top',
|
||||
dropdownVisible: false,
|
||||
showNestedOptions: false,
|
||||
selected: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
handleScroll() {
|
||||
this.setState({
|
||||
isActionsOpen: false,
|
||||
@ -513,8 +514,7 @@ class UserDropdown extends PureComponent {
|
||||
if (!isDropdownVisible && scrollArea) {
|
||||
const { offsetTop, offsetHeight } = dropdownTrigger;
|
||||
const offsetPageTop = (offsetTop + offsetHeight) - scrollArea.scrollTop;
|
||||
|
||||
nextState.dropdownOffset = window.innerHeight - offsetPageTop;
|
||||
|
||||
nextState.dropdownDirection = 'bottom';
|
||||
}
|
||||
|
||||
@ -533,6 +533,10 @@ class UserDropdown extends PureComponent {
|
||||
return isActionsOpen && !dropdownVisible;
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
this.setState({ selected: null });
|
||||
}
|
||||
|
||||
renderUserAvatar() {
|
||||
const {
|
||||
normalizeEmojiName,
|
||||
@ -596,6 +600,7 @@ class UserDropdown extends PureComponent {
|
||||
|
||||
const userItemContentsStyle = {};
|
||||
|
||||
userItemContentsStyle[styles.selected] = this.state.selected === true;
|
||||
userItemContentsStyle[styles.dropdown] = true;
|
||||
userItemContentsStyle[styles.userListItem] = !isActionsOpen;
|
||||
userItemContentsStyle[styles.usertListItemWithMenu] = isActionsOpen;
|
||||
@ -620,7 +625,7 @@ class UserDropdown extends PureComponent {
|
||||
<div
|
||||
data-test={isMe(user.userId) ? 'userListItemCurrent' : 'userListItem'}
|
||||
className={!actions.length ? styles.userListItem : null}
|
||||
style={{ direction: isRTL ? 'rtl' : 'ltr' }}
|
||||
style={{ direction: isRTL ? 'rtl' : 'ltr', width: '100%' }}
|
||||
>
|
||||
<div className={styles.userItemContents}>
|
||||
<div className={styles.userAvatar}>
|
||||
@ -642,42 +647,24 @@ class UserDropdown extends PureComponent {
|
||||
);
|
||||
|
||||
if (!actions.length) return contents;
|
||||
const placement = `right ${dropdownDirection}`;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
ref={(ref) => { this.dropdown = ref; }}
|
||||
keepOpen={isActionsOpen || showNestedOptions}
|
||||
onShow={this.onActionsShow}
|
||||
onHide={this.onActionsHide}
|
||||
className={userItemContentsStyle}
|
||||
autoFocus={false}
|
||||
aria-haspopup="true"
|
||||
aria-live="assertive"
|
||||
aria-label={userAriaLabel}
|
||||
aria-relevant="additions"
|
||||
placement={placement}
|
||||
getContent={dropdownContent => this.dropdownContent = dropdownContent}
|
||||
tethered
|
||||
>
|
||||
<Dropdown.DropdownTrigger>
|
||||
{contents}
|
||||
</Dropdown.DropdownTrigger>
|
||||
<Dropdown.DropdownContent
|
||||
style={{
|
||||
visibility: dropdownVisible ? 'visible' : 'hidden',
|
||||
}}
|
||||
className={styles.dropdownContent}
|
||||
placement={placement}
|
||||
>
|
||||
<Dropdown.DropdownList
|
||||
ref={(ref) => { this.list = ref; }}
|
||||
getDropdownMenuParent={this.getDropdownMenuParent}
|
||||
onActionsHide={this.onActionsHide}
|
||||
>
|
||||
{actions}
|
||||
</Dropdown.DropdownList>
|
||||
</Dropdown.DropdownContent>
|
||||
</Dropdown>
|
||||
<BBBMenu
|
||||
trigger={
|
||||
<div
|
||||
tabIndex={-1}
|
||||
onClick={() => this.setState({ selected: true })}
|
||||
className={cx(userItemContentsStyle)}
|
||||
aria-controls="default-dropdown-menu"
|
||||
aria-haspopup="true"
|
||||
style={{ width: '100%', marginLeft: '.5rem'}}>
|
||||
{contents}
|
||||
</div>
|
||||
}
|
||||
actions={actions}
|
||||
selectedEmoji={user.emoji}
|
||||
onCloseCallback={() => this.setState({ selected: false, showNestedOptions: false })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -146,16 +146,26 @@
|
||||
flex-grow: 0;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
margin: 0 0 1px var(--lg-padding-y);
|
||||
padding: var(--lg-padding-y) 0 var(--lg-padding-y) var(--lg-padding-y);
|
||||
padding: 3px;
|
||||
|
||||
[dir="rtl"] & {
|
||||
padding: var(--lg-padding-y) var(--lg-padding-y) var(--lg-padding-y) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: var(--list-item-bg-hover);
|
||||
border-top-left-radius: var(--sm-padding-y);
|
||||
border-bottom-left-radius: var(--sm-padding-y);
|
||||
|
||||
&:focus {
|
||||
box-shadow: inset 0 0 0 var(--border-size) var(--item-focus-border), inset 1px 0 0 1px var(--item-focus-border);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: static;
|
||||
padding: .45rem;
|
||||
}
|
||||
|
||||
.dropdownContent {
|
||||
|
@ -5,7 +5,7 @@
|
||||
flex-grow: 1;
|
||||
margin: 0 0 0 var(--sm-padding-x);
|
||||
justify-content: center;
|
||||
font-size: 0.95rem;
|
||||
font-size: 90%;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: 0 var(--sm-padding-x) 0 0;
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
.userNameMain {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
font-size: 90%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
@ -12,6 +12,7 @@ import CaptionsService from '/imports/ui/components/captions/service';
|
||||
import CaptionsWriterMenu from '/imports/ui/components/captions/writer-menu/container';
|
||||
import { styles } from './styles';
|
||||
import { getUserNamesLink } from '/imports/ui/components/user-list/service';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
@ -151,16 +152,17 @@ class UserOptions extends PureComponent {
|
||||
|
||||
onSaveUserNames() {
|
||||
const { intl, meetingName } = this.props;
|
||||
const lang = Settings.application.locale;
|
||||
const date = new Date();
|
||||
|
||||
const dateString = lang ? date.toLocaleDateString(lang) : date.toLocaleDateString();
|
||||
const timeString = lang ? date.toLocaleTimeString(lang) : date.toLocaleTimeString();
|
||||
|
||||
getUserNamesLink(
|
||||
intl.formatMessage(intlMessages.savedNamesListTitle,
|
||||
{
|
||||
0: meetingName,
|
||||
1: `${date.toLocaleDateString(
|
||||
document.documentElement.lang,
|
||||
)}:${date.toLocaleTimeString(
|
||||
document.documentElement.lang,
|
||||
)}`,
|
||||
1: `${dateString}:${timeString}`,
|
||||
}),
|
||||
intl.formatMessage(intlMessages.sortedFirstNameHeading),
|
||||
intl.formatMessage(intlMessages.sortedLastNameHeading),
|
||||
|
@ -534,9 +534,14 @@ class VideoService {
|
||||
}
|
||||
|
||||
webcamsOnlyForModerator() {
|
||||
const m = Meetings.findOne({ meetingId: Auth.meetingID },
|
||||
const meeting = Meetings.findOne({ meetingId: Auth.meetingID },
|
||||
{ fields: { 'usersProp.webcamsOnlyForModerator': 1 } });
|
||||
return m?.usersProp ? m.usersProp.webcamsOnlyForModerator : false;
|
||||
const user = Users.findOne({ userId: Auth.userID }, { fields: { locked: 1, role: 1 } });
|
||||
|
||||
if (meeting?.usersProp && user?.role !== ROLE_MODERATOR && user?.locked) {
|
||||
return meeting.usersProp.webcamsOnlyForModerator;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
|
@ -6,6 +6,7 @@ import styles from './styles.scss';
|
||||
import { ACTIONS, CAMERADOCK_POSITION } from '../layout/enums';
|
||||
import DropAreaContainer from './drop-areas/container';
|
||||
import VideoProviderContainer from '/imports/ui/components/video-provider/container';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
|
||||
const WebcamComponent = ({
|
||||
cameraDock,
|
||||
@ -18,10 +19,47 @@ const WebcamComponent = ({
|
||||
const [isFullscreen, setIsFullScreen] = useState(false);
|
||||
const [resizeStart, setResizeStart] = useState({ width: 0, height: 0 });
|
||||
|
||||
const lastSize = Storage.getItem('webcamSize') || { width: 0, height: 0 };
|
||||
const { width: lastWidth, height: lastHeight } = lastSize;
|
||||
|
||||
const isCameraTopOrBottom = cameraDock.position === CAMERADOCK_POSITION.CONTENT_TOP
|
||||
|| cameraDock.position === CAMERADOCK_POSITION.CONTENT_BOTTOM;
|
||||
const isCameraLeftOrRight = cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT
|
||||
|| cameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT;
|
||||
|
||||
useEffect(() => {
|
||||
setIsFullScreen(fullscreen.group === 'webcams');
|
||||
}, [fullscreen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isCameraTopOrBottom && lastHeight > 0) {
|
||||
newLayoutContextDispatch(
|
||||
{
|
||||
type: ACTIONS.SET_CAMERA_DOCK_SIZE,
|
||||
value: {
|
||||
width: cameraDock.width,
|
||||
height: lastHeight,
|
||||
browserWidth: window.innerWidth,
|
||||
browserHeight: window.innerHeight,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
if (isCameraLeftOrRight && lastWidth > 0) {
|
||||
newLayoutContextDispatch(
|
||||
{
|
||||
type: ACTIONS.SET_CAMERA_DOCK_SIZE,
|
||||
value: {
|
||||
width: lastWidth,
|
||||
height: cameraDock.height,
|
||||
browserWidth: window.innerWidth,
|
||||
browserHeight: window.innerHeight,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}, [cameraDock.position, lastWidth, lastHeight]);
|
||||
|
||||
const onResizeHandle = (deltaWidth, deltaHeight) => {
|
||||
if (cameraDock.resizableEdge.top || cameraDock.resizableEdge.bottom) {
|
||||
newLayoutContextDispatch(
|
||||
@ -124,14 +162,20 @@ const WebcamComponent = ({
|
||||
onResizeHandle(d.width, d.height);
|
||||
}}
|
||||
onResizeStop={() => {
|
||||
if (isCameraTopOrBottom) {
|
||||
Storage.setItem('webcamSize', { width: lastWidth, height: cameraDock.height });
|
||||
}
|
||||
if (isCameraLeftOrRight) {
|
||||
Storage.setItem('webcamSize', { width: cameraDock.width, height: lastHeight });
|
||||
}
|
||||
setResizeStart({ width: 0, height: 0 });
|
||||
setTimeout(() => setIsResizing(false), 500);
|
||||
}}
|
||||
enable={{
|
||||
top: !isFullscreen && !isDragging && cameraDock.resizableEdge.top,
|
||||
bottom: !isFullscreen && !isDragging && cameraDock.resizableEdge.bottom,
|
||||
left: !isFullscreen && !isDragging && cameraDock.resizableEdge.left,
|
||||
right: !isFullscreen && !isDragging && cameraDock.resizableEdge.right,
|
||||
top: !isFullscreen && !isDragging && !swapLayout && cameraDock.resizableEdge.top,
|
||||
bottom: !isFullscreen && !isDragging && !swapLayout && cameraDock.resizableEdge.bottom,
|
||||
left: !isFullscreen && !isDragging && !swapLayout && cameraDock.resizableEdge.left,
|
||||
right: !isFullscreen && !isDragging && !swapLayout && cameraDock.resizableEdge.right,
|
||||
topLeft: false,
|
||||
topRight: false,
|
||||
bottomLeft: false,
|
||||
|
@ -10,7 +10,7 @@ const DropAreaContainer = () => {
|
||||
|
||||
return (
|
||||
Object.keys(dropZoneAreas).map((objectKey) => (
|
||||
<DropArea id={objectKey} style={dropZoneAreas[objectKey]} />
|
||||
<DropArea key={objectKey} id={objectKey} style={dropZoneAreas[objectKey]} />
|
||||
))
|
||||
);
|
||||
};
|
||||
|
@ -372,8 +372,12 @@ class AudioManager {
|
||||
|
||||
if (!this.logAudioJoinTime) {
|
||||
this.logAudioJoinTime = true;
|
||||
logger.info({ logCode: 'audio_mic_join_time' }, 'Time needed to '
|
||||
+ `connect audio (seconds): ${secondsToActivateAudio}`);
|
||||
logger.info({
|
||||
logCode: 'audio_mic_join_time',
|
||||
extraInfo: {
|
||||
secondsToActivateAudio,
|
||||
},
|
||||
}, `Time needed to connect audio (seconds): ${secondsToActivateAudio}`);
|
||||
}
|
||||
|
||||
if (!this.isEchoTest) {
|
||||
|
@ -225,12 +225,16 @@ class Auth {
|
||||
|
||||
makeCall('validateAuthToken', this.meetingID, this.userID, this.token, this.externUserID);
|
||||
|
||||
Meteor.subscribe('auth-token-validation', { meetingId: this.meetingID, userId: this.userID });
|
||||
const authTokenSubscription = Meteor.subscribe('auth-token-validation', { meetingId: this.meetingID, userId: this.userID });
|
||||
Meteor.subscribe('current-user');
|
||||
|
||||
Tracker.autorun((c) => {
|
||||
computation = c;
|
||||
|
||||
if (!authTokenSubscription.ready()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selector = {
|
||||
connectionId: Meteor.connection._lastSessionId,
|
||||
};
|
||||
|
@ -30,6 +30,7 @@
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@browser-bunyan/server-stream": "^1.6.1",
|
||||
"@jitsi/sdp-interop": "0.1.14",
|
||||
"@material-ui/core": "^4.11.4",
|
||||
"autoprefixer": "^10.2.5",
|
||||
"axios": "^0.21.1",
|
||||
"babel-runtime": "~6.26.0",
|
||||
|
@ -49,6 +49,7 @@
|
||||
"app.captions.pad.dictationStop": "Stop dictation",
|
||||
"app.captions.pad.dictationOnDesc": "Turns speech recognition on",
|
||||
"app.captions.pad.dictationOffDesc": "Turns speech recognition off",
|
||||
"app.captions.pad.speechRecognitionStop": "Speech recognition stopped due to the browser incompatibility or some time of silence",
|
||||
"app.textInput.sendLabel": "Send",
|
||||
"app.note.title": "Shared Notes",
|
||||
"app.note.label": "Note",
|
||||
@ -144,6 +145,8 @@
|
||||
"app.meeting.meetingTimeRemaining": "Meeting time remaining: {0}",
|
||||
"app.meeting.meetingTimeHasEnded": "Time ended. Meeting will close soon",
|
||||
"app.meeting.endedByUserMessage": "This session was ended by {0}",
|
||||
"app.meeting.endedByNoModeratorMessageSingular": "The meeting has ended due to no moderator being present after one minute",
|
||||
"app.meeting.endedByNoModeratorMessagePlural": "The meeting has ended due to no moderator being present after {0} minutes",
|
||||
"app.meeting.endedMessage": "You will be forwarded back to the home screen",
|
||||
"app.meeting.alertMeetingEndsUnderMinutesSingular": "Meeting is closing in one minute.",
|
||||
"app.meeting.alertMeetingEndsUnderMinutesPlural": "Meeting is closing in {0} minutes.",
|
||||
|
@ -1,14 +0,0 @@
|
||||
# meeting credentials
|
||||
BBB_SERVER_URL=""
|
||||
BBB_SHARED_SECRET=""
|
||||
|
||||
# browserless credentials
|
||||
BROWSERLESS_ENABLED=false # true/false
|
||||
BROWSERLESS_URL= # ip:port
|
||||
BROWSERLESS_TOKEN= # token
|
||||
|
||||
# collecting metrics
|
||||
BBB_COLLECT_METRICS=true # (true/false): true to collect metrics
|
||||
TEST_FOLDER=data # the metrics output folder
|
||||
GENERATE_EVIDENCES=true # (true/false): true to generate evidences
|
||||
DEBUG=true # (true/false): true to enable console debugging
|
10
bigbluebutton-html5/tests/puppeteer/.gitignore
vendored
10
bigbluebutton-html5/tests/puppeteer/.gitignore
vendored
@ -1,10 +0,0 @@
|
||||
node_modules/
|
||||
screenshots/*
|
||||
!screenshots/screenshots.txt
|
||||
downloads/*
|
||||
!downloads/downloads.txt
|
||||
.directory
|
||||
.env
|
||||
media/*
|
||||
data/
|
||||
__image_snapshots__/
|
@ -1,2 +0,0 @@
|
||||
TESTING_SERVER=""
|
||||
TESTING_SECRET=""
|
@ -1,89 +0,0 @@
|
||||
|
||||
|
||||
const Page = require('./page');
|
||||
|
||||
const pageObject = new Page();
|
||||
const chai = require('chai');
|
||||
|
||||
class ChatPage extends Page {
|
||||
get publicChatSelector() {
|
||||
return '#message-input';
|
||||
}
|
||||
|
||||
get publicChatElement() {
|
||||
return $(this.publicChatSelector);
|
||||
}
|
||||
|
||||
sendPublicChatMessage(message) {
|
||||
this.publicChatElement.setValue(message);
|
||||
this.sendMessageButtonElement.click();
|
||||
}
|
||||
|
||||
// ////////
|
||||
|
||||
get chatDropdownTriggerSelector() {
|
||||
return '[data-test=chatDropdownTrigger]';
|
||||
}
|
||||
|
||||
get chatDropdownTriggerElement() {
|
||||
return $(this.chatDropdownTriggerSelector);
|
||||
}
|
||||
|
||||
triggerChatDropdown() {
|
||||
this.chatDropdownTriggerElement.click();
|
||||
}
|
||||
|
||||
// ////////
|
||||
|
||||
get clearChatButtonSelector() {
|
||||
return '[data-test=chatClear]';
|
||||
}
|
||||
|
||||
get clearChatButtonElement() {
|
||||
return $(this.clearChatButtonSelector);
|
||||
}
|
||||
|
||||
clearChat() {
|
||||
this.clearChatButtonElement.click();
|
||||
}
|
||||
|
||||
// ////////
|
||||
|
||||
get saveChatButtonSelector() {
|
||||
return '[data-test=chatSave]';
|
||||
}
|
||||
|
||||
get saveChatButtonElement() {
|
||||
return $(this.saveChatButtonSelector);
|
||||
}
|
||||
|
||||
saveChat() {
|
||||
this.saveChatButtonElement.click();
|
||||
}
|
||||
|
||||
// ////////
|
||||
|
||||
get copyChatButtonSelector() {
|
||||
return '[data-test=chatCopy]';
|
||||
}
|
||||
|
||||
get copyChatButtonElement() {
|
||||
return $(this.copyChatButtonSelector);
|
||||
}
|
||||
|
||||
copyChat() {
|
||||
this.copyChatButtonElement.click();
|
||||
}
|
||||
|
||||
// ////////
|
||||
|
||||
get sendMessageButtonSelector() {
|
||||
return '[data-test=sendMessageButton]';
|
||||
}
|
||||
|
||||
get sendMessageButtonElement() {
|
||||
return $(this.sendMessageButtonSelector);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ChatPage();
|
@ -1,85 +0,0 @@
|
||||
|
||||
const chai = require('chai');
|
||||
const sha1 = require('sha1');
|
||||
const Page = require('./page');
|
||||
|
||||
const pageObject = new Page();
|
||||
|
||||
const WAIT_TIME = 10000;
|
||||
|
||||
const generateRandomMeetingId = function () {
|
||||
return `random-${Math.floor(1000000 + 9000000 * Math.random())}`;
|
||||
};
|
||||
|
||||
const createMeeting = function () {
|
||||
const meetingId = generateRandomMeetingId();
|
||||
const query = `name=${meetingId}&meetingID=${meetingId}&attendeePW=ap`
|
||||
+ '&moderatorPW=mp&joinViaHtml5=true&welcome=Welcome';
|
||||
const checksum = sha1(`create${query}${process.env.TESTING_SECRET}`);
|
||||
const url = `${process.env.TESTING_SERVER}create?${query}&checksum=${checksum}`;
|
||||
|
||||
browser.url(url);
|
||||
browser.waitForExist('body', WAIT_TIME);
|
||||
chai.expect($('body').getText()).to.include('<returncode>SUCCESS</returncode>');
|
||||
|
||||
return meetingId;
|
||||
};
|
||||
|
||||
class LandingPage extends Page {
|
||||
get meetingNameInputSelector() {
|
||||
return 'input[name=meetingname]';
|
||||
}
|
||||
|
||||
get meetingNameInputElement() {
|
||||
return $(this.meetingNameInputSelector);
|
||||
}
|
||||
|
||||
// ////////
|
||||
|
||||
get usernameInputSelector() {
|
||||
return 'input[name=username]';
|
||||
}
|
||||
|
||||
get usernameInputElement() {
|
||||
return $(this.usernameInputSelector);
|
||||
}
|
||||
|
||||
// ////////
|
||||
|
||||
joinWithButtonClick() {
|
||||
this.joinButtonElement.click();
|
||||
}
|
||||
|
||||
joinWithEnterKey() {
|
||||
pageObject.pressEnter();
|
||||
}
|
||||
|
||||
// ////////
|
||||
|
||||
get joinButtonSelector() {
|
||||
return 'input[type=submit]';
|
||||
}
|
||||
|
||||
get joinButtonElement() {
|
||||
return $(this.joinButtonSelector);
|
||||
}
|
||||
|
||||
// ////////
|
||||
|
||||
joinMeeting(meetingId, fullName) {
|
||||
const query = `fullName=${fullName}&joinViaHtml5=true`
|
||||
+ `&meetingID=${meetingId}&password=mp`;
|
||||
const checksum = sha1(`join${query}${process.env.TESTING_SECRET}`);
|
||||
const url = `${process.env.TESTING_SERVER}join?${query}&checksum=${checksum}`;
|
||||
browser.url(url);
|
||||
}
|
||||
|
||||
// ////////
|
||||
|
||||
joinClient(fullName) {
|
||||
const meetingId = createMeeting();
|
||||
this.joinMeeting(meetingId, fullName);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new LandingPage();
|
@ -1,38 +0,0 @@
|
||||
|
||||
|
||||
const Page = require('./page');
|
||||
|
||||
const pageObject = new Page();
|
||||
const chai = require('chai');
|
||||
|
||||
class ModalPage extends Page {
|
||||
get modalCloseSelector() {
|
||||
return 'i.icon-bbb-close';
|
||||
}
|
||||
|
||||
get modalCloseElement() {
|
||||
return $(this.modalCloseSelector);
|
||||
}
|
||||
|
||||
closeAudioModal() {
|
||||
this.modalCloseElement.click();
|
||||
}
|
||||
|
||||
get meetingEndedModalTitleSelector() {
|
||||
return '[data-test=meetingEndedModalTitle]';
|
||||
}
|
||||
|
||||
get aboutModalSelector() {
|
||||
return '[aria-label=About]';
|
||||
}
|
||||
|
||||
get modalConfirmButtonSelector() {
|
||||
return '[data-test=modalConfirmButton]';
|
||||
}
|
||||
|
||||
get modalConfirmButtonElement() {
|
||||
return browser.element(this.modalConfirmButtonSelector);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ModalPage();
|
@ -1,9 +0,0 @@
|
||||
|
||||
|
||||
class Page {
|
||||
pressEnter() {
|
||||
browser.keys('Enter');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Page;
|
@ -1,93 +0,0 @@
|
||||
|
||||
|
||||
const Page = require('./page');
|
||||
|
||||
const pageObject = new Page();
|
||||
const chai = require('chai');
|
||||
|
||||
class SettingsPage extends Page {
|
||||
// open the settings dropdown
|
||||
get openSettingsDropdownSelector() {
|
||||
return 'i.icon-bbb-more';
|
||||
}
|
||||
|
||||
get openSettingsDropdownElement() {
|
||||
return $(this.openSettingsDropdownSelector);
|
||||
}
|
||||
|
||||
openSettingsDropdown() {
|
||||
this.openSettingsDropdownElement.click();
|
||||
}
|
||||
|
||||
// ////////
|
||||
|
||||
get endMeetingButtonSelector() {
|
||||
return 'i.icon-bbb-application';
|
||||
}
|
||||
|
||||
get endMeetingButtonElement() {
|
||||
return $(this.endMeetingButtonSelector);
|
||||
}
|
||||
|
||||
clickEndMeetingButton() {
|
||||
this.endMeetingButtonElement.click();
|
||||
}
|
||||
|
||||
// ////////
|
||||
|
||||
get confirmEndMeetingSelector() {
|
||||
return '[data-test=confirmEndMeeting]';
|
||||
}
|
||||
|
||||
get confirmEndMeetingElement() {
|
||||
return $(this.confirmEndMeetingSelector);
|
||||
}
|
||||
|
||||
confirmEndMeeting() {
|
||||
this.confirmEndMeetingElement.click();
|
||||
}
|
||||
|
||||
// ////////
|
||||
|
||||
get logoutButtonSelector() {
|
||||
return 'i.icon-bbb-logout';
|
||||
}
|
||||
|
||||
get logoutButtonElement() {
|
||||
return $(this.logoutButtonSelector);
|
||||
}
|
||||
|
||||
clickLogoutButton() {
|
||||
this.logoutButtonElement.click();
|
||||
}
|
||||
|
||||
// ////////
|
||||
|
||||
get settingsButtonSelector() {
|
||||
return 'i.icon-bbb-settings';
|
||||
}
|
||||
|
||||
get settingsButtonElement() {
|
||||
return $(this.settingsButtonSelector);
|
||||
}
|
||||
|
||||
clickSettingsButton() {
|
||||
this.settingsButtonElement.click();
|
||||
}
|
||||
|
||||
// ////////
|
||||
|
||||
get languageSelectSelector() {
|
||||
return '#langSelector';
|
||||
}
|
||||
|
||||
get languageSelectElement() {
|
||||
return $(this.languageSelectSelector);
|
||||
}
|
||||
|
||||
clickLanguageSelect() {
|
||||
this.languageSelectElement.click();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new SettingsPage();
|
@ -1,81 +0,0 @@
|
||||
|
||||
const chai = require('chai');
|
||||
const clipboardy = require('clipboardy');
|
||||
const LandingPage = require('../pageobjects/landing.page');
|
||||
const ModalPage = require('../pageobjects/modal.page');
|
||||
const ChatPage = require('../pageobjects/chat.page');
|
||||
const Utils = require('../utils');
|
||||
|
||||
const WAIT_TIME = 10000;
|
||||
const message = 'Hello';
|
||||
|
||||
const loginWithoutAudio = function (username) {
|
||||
LandingPage.joinClient(username);
|
||||
|
||||
// close audio modal
|
||||
browser.waitForExist(ModalPage.modalCloseSelector, WAIT_TIME);
|
||||
ModalPage.closeAudioModal();
|
||||
};
|
||||
|
||||
describe('Chat', () => {
|
||||
beforeEach(() => {
|
||||
Utils.configureViewport();
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
|
||||
});
|
||||
|
||||
it('should be able to send a message',
|
||||
() => {
|
||||
const username = 'chatUser1';
|
||||
loginWithoutAudio(username);
|
||||
|
||||
browser.waitForExist(ChatPage.publicChatSelector, WAIT_TIME);
|
||||
ChatPage.sendPublicChatMessage(message);
|
||||
});
|
||||
|
||||
it('should be able to save chat',
|
||||
() => {
|
||||
const username = 'chatUser2';
|
||||
loginWithoutAudio(username);
|
||||
|
||||
browser.waitForExist(ChatPage.publicChatSelector, WAIT_TIME);
|
||||
ChatPage.sendPublicChatMessage(message);
|
||||
|
||||
browser.waitForExist(ChatPage.chatDropdownTriggerSelector, WAIT_TIME);
|
||||
ChatPage.triggerChatDropdown();
|
||||
|
||||
browser.waitForExist(ChatPage.saveChatButtonSelector, WAIT_TIME);
|
||||
ChatPage.saveChat();
|
||||
});
|
||||
|
||||
it('should be able to copy chat',
|
||||
() => {
|
||||
const username = 'chatUser3';
|
||||
loginWithoutAudio(username);
|
||||
|
||||
browser.waitForExist(ChatPage.publicChatSelector, WAIT_TIME);
|
||||
ChatPage.sendPublicChatMessage(message);
|
||||
|
||||
browser.waitForExist(ChatPage.chatDropdownTriggerSelector, WAIT_TIME);
|
||||
ChatPage.triggerChatDropdown();
|
||||
|
||||
browser.waitForExist(ChatPage.copyChatButtonSelector, WAIT_TIME);
|
||||
ChatPage.copyChat();
|
||||
const copiedChat = clipboardy.readSync();
|
||||
chai.expect(copiedChat).to.include(`${username} : ${message}`);
|
||||
});
|
||||
|
||||
it('should be able to clear chat',
|
||||
() => {
|
||||
const username = 'chatUser4';
|
||||
loginWithoutAudio(username);
|
||||
|
||||
browser.waitForExist(ChatPage.publicChatSelector, WAIT_TIME);
|
||||
ChatPage.sendPublicChatMessage(message);
|
||||
|
||||
browser.waitForExist(ChatPage.chatDropdownTriggerSelector, WAIT_TIME);
|
||||
ChatPage.triggerChatDropdown();
|
||||
|
||||
browser.waitForExist(ChatPage.clearChatButtonSelector, WAIT_TIME);
|
||||
ChatPage.clearChat();
|
||||
});
|
||||
});
|
@ -1,35 +0,0 @@
|
||||
const LandingPage = require('../pageobjects/landing.page');
|
||||
const ModalPage = require('../pageobjects/modal.page');
|
||||
const ChatPage = require('../pageobjects/chat.page');
|
||||
const Utils = require('../utils');
|
||||
|
||||
const WAIT_TIME = 10000;
|
||||
|
||||
const checkFullscreen = () => document.fullscreen;
|
||||
|
||||
const loginWithoutAudio = function (username) {
|
||||
LandingPage.joinClient(username);
|
||||
|
||||
// close audio modal
|
||||
browser.waitForExist(ModalPage.modalCloseSelector, WAIT_TIME);
|
||||
ModalPage.closeAudioModal();
|
||||
};
|
||||
|
||||
describe('Presentation', () => {
|
||||
beforeEach(() => {
|
||||
Utils.configureViewport();
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
|
||||
});
|
||||
|
||||
it('should be able to enter fullscreen',
|
||||
() => {
|
||||
const username = 'presentationUser';
|
||||
loginWithoutAudio(username);
|
||||
|
||||
browser.waitForExist(ChatPage.publicChatSelector, WAIT_TIME);
|
||||
browser.waitForExist('[data-test="presentationFullscreenButton"]', WAIT_TIME);
|
||||
$('[data-test="presentationFullscreenButton"]').click();
|
||||
browser.pause(2000);
|
||||
console.log(browser.execute(checkFullscreen));
|
||||
});
|
||||
});
|
@ -1,106 +0,0 @@
|
||||
|
||||
|
||||
const chai = require('chai');
|
||||
const LandingPage = require('../pageobjects/landing.page');
|
||||
const ModalPage = require('../pageobjects/modal.page');
|
||||
const SettingsPage = require('../pageobjects/settings.page');
|
||||
const Utils = require('../utils');
|
||||
|
||||
const WAIT_TIME = 10000;
|
||||
let errorsCounter = 0;
|
||||
|
||||
const openSettingsDropdown = function () {
|
||||
browser.waitForExist(SettingsPage.openSettingsDropdownSelector, WAIT_TIME);
|
||||
SettingsPage.openSettingsDropdown();
|
||||
};
|
||||
|
||||
const closeAudioModal = function () {
|
||||
browser.waitForExist(ModalPage.modalCloseSelector, WAIT_TIME);
|
||||
ModalPage.closeAudioModal();
|
||||
};
|
||||
|
||||
const openSettingsModal = function () {
|
||||
browser.waitForExist(SettingsPage.settingsButtonSelector, WAIT_TIME);
|
||||
SettingsPage.clickSettingsButton();
|
||||
};
|
||||
|
||||
describe('Settings', () => {
|
||||
beforeAll(() => {
|
||||
Utils.configureViewport();
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 300000;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
LandingPage.joinClient('settingsUser');
|
||||
closeAudioModal();
|
||||
});
|
||||
|
||||
it('should be able to use all locales',
|
||||
() => {
|
||||
openSettingsDropdown();
|
||||
openSettingsModal();
|
||||
browser.waitForExist(`${SettingsPage.languageSelectSelector} option:not([disabled]`, WAIT_TIME);
|
||||
const locales = browser.elements('#langSelector option:not([disabled]').value.map(e => e.getValue());
|
||||
browser.refresh();
|
||||
|
||||
browser.cdp('Console', 'enable');
|
||||
browser.on('Console.messageAdded', (log) => {
|
||||
if (log.message.level === 'error') {
|
||||
console.log(log.message.text);
|
||||
errorsCounter += 1;
|
||||
}
|
||||
});
|
||||
|
||||
locales.forEach((locale) => {
|
||||
errorsCounter = 0;
|
||||
|
||||
closeAudioModal();
|
||||
openSettingsDropdown();
|
||||
openSettingsModal();
|
||||
|
||||
browser.waitForExist(SettingsPage.languageSelectSelector, WAIT_TIME);
|
||||
SettingsPage.clickLanguageSelect();
|
||||
browser.waitForExist(`option[value=${locale}]`, WAIT_TIME);
|
||||
|
||||
$(`option[value=${locale}]`).click();
|
||||
|
||||
browser.waitForExist(ModalPage.modalConfirmButtonSelector, WAIT_TIME);
|
||||
ModalPage.modalConfirmButtonElement.click();
|
||||
|
||||
browser.pause(500);
|
||||
browser.refresh();
|
||||
browser.pause(500);
|
||||
console.log(`[switching to ${locale}] number of errors: ${errorsCounter}`);
|
||||
|
||||
chai.expect(errorsCounter < 5).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to end meeting and get confirmation',
|
||||
() => {
|
||||
openSettingsDropdown();
|
||||
|
||||
// click End Meeting
|
||||
browser.waitForExist(SettingsPage.endMeetingButtonSelector, WAIT_TIME);
|
||||
SettingsPage.clickEndMeetingButton();
|
||||
|
||||
// confirm
|
||||
browser.waitForExist(SettingsPage.confirmEndMeetingSelector, WAIT_TIME);
|
||||
SettingsPage.confirmEndMeeting();
|
||||
|
||||
// check the confirmation page
|
||||
browser.waitForExist(ModalPage.meetingEndedModalTitleSelector, WAIT_TIME);
|
||||
});
|
||||
|
||||
it('should be able to logout and get confirmation',
|
||||
() => {
|
||||
openSettingsDropdown();
|
||||
|
||||
// click Logout
|
||||
browser.waitForExist(SettingsPage.logoutButtonSelector, WAIT_TIME);
|
||||
SettingsPage.clickLogoutButton();
|
||||
|
||||
// check the confirmation page
|
||||
browser.waitForExist(ModalPage.meetingEndedModalTitleSelector, WAIT_TIME);
|
||||
});
|
||||
});
|
@ -1,12 +0,0 @@
|
||||
|
||||
|
||||
class Utils {
|
||||
configureViewport() {
|
||||
browser.setViewportSize({
|
||||
width: 1366,
|
||||
height: 768,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Utils();
|
@ -1,11 +0,0 @@
|
||||
require('dotenv').config({ path: './tests/webdriverio/.testing-env' });
|
||||
|
||||
exports.config = {
|
||||
specs: ['tests/webdriverio/specs/**/*.spec.js'],
|
||||
capabilities: [{
|
||||
browserName: 'chrome',
|
||||
}],
|
||||
services: ['devtools'],
|
||||
framework: 'jasmine',
|
||||
reporters: ['spec'],
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user