Merge branch 'develop' into activity-report

This commit is contained in:
Gustavo Trott 2021-07-21 14:20:27 -03:00
commit b0f9835ffd
170 changed files with 1369 additions and 1184 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import Service from './service';
import UserListService from '/imports/ui/components/user-list/service';
import ExternalVideoService from '/imports/ui/components/external-video-player/service';
import CaptionsService from '/imports/ui/components/captions/service';
import { 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,
}
}
/>

View File

@ -287,6 +287,7 @@ class BreakoutRoom extends PureComponent {
roomList.removeEventListener('keydown', this.handleMoveEvent, true);
}
}
this.handleDismiss();
}
componentDidUpdate(prevProps, prevstate) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,7 +41,7 @@ const DEFAULT_VALUES = {
sidebarContentHeight: '100%',
sidebarContentTop: 0,
sidebarContentTabOrder: 2,
sidebarContentPanel: PANELS.CHAT,
sidebarContentPanel: PANELS.NONE,
};
export default DEFAULT_VALUES;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -0,0 +1,6 @@
.button {
top: 5px;
left: 5px;
position: absolute;
z-index: 2;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +0,0 @@
node_modules/
screenshots/*
!screenshots/screenshots.txt
downloads/*
!downloads/downloads.txt
.directory
.env
media/*
data/
__image_snapshots__/

View File

@ -1,2 +0,0 @@
TESTING_SERVER=""
TESTING_SECRET=""

View File

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

View File

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

View File

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

View File

@ -1,9 +0,0 @@
class Page {
pressEnter() {
browser.keys('Enter');
}
}
module.exports = Page;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +0,0 @@
class Utils {
configureViewport() {
browser.setViewportSize({
width: 1366,
height: 768,
});
}
}
module.exports = new Utils();

View File

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