Merge remote-tracking branch 'upstream/v3.0.x-release' into publish-poll-open-chat
This commit is contained in:
commit
872924e537
@ -104,7 +104,7 @@ object Polls {
|
||||
} yield {
|
||||
val pageId = if (poll.id.contains("deskshare")) "deskshare" else page.id
|
||||
val updatedShape = shape + ("whiteboardId" -> pageId)
|
||||
val annotation = new AnnotationVO(poll.id, updatedShape, pageId, requesterId)
|
||||
val annotation = new AnnotationVO(s"shape:poll-result-${poll.id}", updatedShape, pageId, requesterId)
|
||||
annotation
|
||||
}
|
||||
}
|
||||
@ -253,12 +253,13 @@ object Polls {
|
||||
|
||||
private def pollResultToWhiteboardShape(result: SimplePollResultOutVO): scala.collection.immutable.Map[String, Object] = {
|
||||
val shape = new scala.collection.mutable.HashMap[String, Object]()
|
||||
shape += "numRespondents" -> new Integer(result.numRespondents)
|
||||
shape += "numResponders" -> new Integer(result.numResponders)
|
||||
shape += "numRespondents" -> Integer.valueOf(result.numRespondents)
|
||||
shape += "numResponders" -> Integer.valueOf(result.numResponders)
|
||||
shape += "questionType" -> result.questionType
|
||||
shape += "questionText" -> result.questionText
|
||||
shape += "id" -> result.id
|
||||
shape += "questionText" -> result.questionText.getOrElse("")
|
||||
shape += "id" -> s"shape:poll-result-${result.id}"
|
||||
shape += "answers" -> result.answers
|
||||
shape += "type" -> "geo"
|
||||
shape.toMap
|
||||
}
|
||||
|
||||
|
@ -85,6 +85,9 @@ class RedisRecorderActor(
|
||||
case m: UserLeftMeetingEvtMsg => handleUserLeftMeetingEvtMsg(m)
|
||||
case m: PresenterAssignedEvtMsg => handlePresenterAssignedEvtMsg(m)
|
||||
case m: UserEmojiChangedEvtMsg => handleUserEmojiChangedEvtMsg(m)
|
||||
case m: UserAwayChangedEvtMsg => handleUserAwayChangedEvtMsg(m)
|
||||
case m: UserRaiseHandChangedEvtMsg => handleUserRaiseHandChangedEvtMsg(m)
|
||||
case m: UserReactionEmojiChangedEvtMsg => handleUserReactionEmojiChangedEvtMsg(m)
|
||||
case m: UserRoleChangedEvtMsg => handleUserRoleChangedEvtMsg(m)
|
||||
case m: UserBroadcastCamStartedEvtMsg => handleUserBroadcastCamStartedEvtMsg(m)
|
||||
case m: UserBroadcastCamStoppedEvtMsg => handleUserBroadcastCamStoppedEvtMsg(m)
|
||||
@ -379,6 +382,18 @@ class RedisRecorderActor(
|
||||
handleUserStatusChange(msg.header.meetingId, msg.body.userId, "emojiStatus", msg.body.emoji)
|
||||
}
|
||||
|
||||
private def handleUserAwayChangedEvtMsg(msg: UserAwayChangedEvtMsg) {
|
||||
handleUserStatusChange(msg.header.meetingId, msg.body.userId, "away", if (msg.body.away) "true" else "false")
|
||||
}
|
||||
|
||||
private def handleUserRaiseHandChangedEvtMsg(msg: UserRaiseHandChangedEvtMsg) {
|
||||
handleUserStatusChange(msg.header.meetingId, msg.body.userId, "raiseHand", if (msg.body.raiseHand) "true" else "false")
|
||||
}
|
||||
|
||||
private def handleUserReactionEmojiChangedEvtMsg(msg: UserReactionEmojiChangedEvtMsg) {
|
||||
handleUserStatusChange(msg.header.meetingId, msg.body.userId, "reactionEmoji", msg.body.reactionEmoji)
|
||||
}
|
||||
|
||||
private def handleUserRoleChangedEvtMsg(msg: UserRoleChangedEvtMsg) {
|
||||
handleUserStatusChange(msg.header.meetingId, msg.body.userId, "role", msg.body.role)
|
||||
}
|
||||
|
@ -181,6 +181,7 @@ public class MeetingService implements MessageListener {
|
||||
removedUser.meetingId = us.meetingID;
|
||||
removedUser.userId = us.internalUserId;
|
||||
removedUser.sessionToken = us.authToken;
|
||||
removedUser.role = us.role;
|
||||
removedSessions.put(token, removedUser);
|
||||
sessions.remove(token);
|
||||
} else {
|
||||
|
@ -23,6 +23,11 @@ public class UserSessionBasicData {
|
||||
public String sessionToken = null;
|
||||
public String userId = null;
|
||||
public String meetingId = null;
|
||||
public String role = null;
|
||||
|
||||
public Boolean isModerator() {
|
||||
return "MODERATOR".equalsIgnoreCase(this.role);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return meetingId + " " + userId + " " + sessionToken;
|
||||
|
@ -90,6 +90,9 @@ export default function Auth() {
|
||||
meeting {
|
||||
name
|
||||
ended
|
||||
learningDashboard {
|
||||
learningDashboardAccessToken
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
@ -131,6 +134,12 @@ export default function Auth() {
|
||||
<span>{curr.guestStatusDetails.guestLobbyMessage}</span>
|
||||
<span>Your position is: {curr.guestStatusDetails.positionInWaitingQueue}</span>
|
||||
</div>
|
||||
} else if(curr.loggedOut) {
|
||||
return <div>
|
||||
{curr.meeting.name}
|
||||
<br/><br/>
|
||||
<span>You left the meeting.</span>
|
||||
</div>
|
||||
} else if(!curr.joined) {
|
||||
return <div>
|
||||
{curr.meeting.name}
|
||||
|
@ -798,6 +798,10 @@ from "meeting"
|
||||
left join "user" "user_ended" on "user_ended"."userId" = "meeting"."endedBy"
|
||||
;
|
||||
|
||||
create view "v_meeting_learningDashboard" as
|
||||
select "meetingId", "learningDashboardAccessToken"
|
||||
from "v_meeting";
|
||||
|
||||
|
||||
-- ===================== CHAT TABLES
|
||||
|
||||
|
@ -10,13 +10,13 @@ cd "$(dirname "$0")"
|
||||
# Install Postgresql
|
||||
apt update
|
||||
apt install postgresql postgresql-contrib -y
|
||||
sudo -u postgres psql -c "alter user postgres password 'bbb_graphql'"
|
||||
sudo -u postgres psql -c "drop database if exists bbb_graphql with (force)"
|
||||
sudo -u postgres psql -c "create database bbb_graphql WITH TEMPLATE template0 LC_COLLATE 'C.UTF-8'"
|
||||
sudo -u postgres psql -c "alter database bbb_graphql set timezone to 'UTC'"
|
||||
sudo -u postgres psql -U postgres -d bbb_graphql -a -f bbb_schema.sql --set ON_ERROR_STOP=on
|
||||
sudo -u postgres psql -c "drop database if exists hasura_app with (force)"
|
||||
sudo -u postgres psql -c "create database hasura_app"
|
||||
runuser -u postgres -- psql -c "alter user postgres password 'bbb_graphql'"
|
||||
runuser -u postgres -- psql -c "drop database if exists bbb_graphql with (force)"
|
||||
runuser -u postgres -- psql -c "create database bbb_graphql WITH TEMPLATE template0 LC_COLLATE 'C.UTF-8'"
|
||||
runuser -u postgres -- psql -c "alter database bbb_graphql set timezone to 'UTC'"
|
||||
runuser -u postgres -- psql -U postgres -d bbb_graphql -a -f bbb_schema.sql --set ON_ERROR_STOP=on
|
||||
runuser -u postgres -- psql -c "drop database if exists hasura_app with (force)"
|
||||
runuser -u postgres -- psql -c "create database hasura_app"
|
||||
|
||||
echo "Creating frontend in bbb_graphql"
|
||||
DATABASE_FRONTEND_USER="bbb_frontend"
|
||||
|
@ -52,6 +52,15 @@ object_relationships:
|
||||
remote_table:
|
||||
name: v_layout
|
||||
schema: public
|
||||
- name: learningDashboard
|
||||
using:
|
||||
manual_configuration:
|
||||
column_mapping:
|
||||
meetingId: meetingId
|
||||
insertion_order: null
|
||||
remote_table:
|
||||
name: v_meeting_learningDashboard
|
||||
schema: public
|
||||
- name: lockSettings
|
||||
using:
|
||||
manual_configuration:
|
||||
|
@ -0,0 +1,20 @@
|
||||
table:
|
||||
name: v_meeting_learningDashboard
|
||||
schema: public
|
||||
select_permissions:
|
||||
- role: bbb_client
|
||||
permission:
|
||||
columns:
|
||||
- learningDashboardAccessToken
|
||||
filter:
|
||||
meetingId:
|
||||
_eq: X-Hasura-ModeratorInMeeting
|
||||
comment: ""
|
||||
- role: not_joined_bbb_client
|
||||
permission:
|
||||
columns:
|
||||
- learningDashboardAccessToken
|
||||
filter:
|
||||
meetingId:
|
||||
_eq: X-Hasura-ModeratorInMeeting
|
||||
comment: ""
|
@ -16,6 +16,7 @@
|
||||
- "!include public_v_meeting_clientSettings.yaml"
|
||||
- "!include public_v_meeting_componentsFlags.yaml"
|
||||
- "!include public_v_meeting_group.yaml"
|
||||
- "!include public_v_meeting_learningDashboard.yaml"
|
||||
- "!include public_v_meeting_lockSettings.yaml"
|
||||
- "!include public_v_meeting_recording.yaml"
|
||||
- "!include public_v_meeting_recordingPolicies.yaml"
|
||||
|
@ -16,13 +16,12 @@ if [ "$hasura_status" = "active" ]; then
|
||||
fi
|
||||
|
||||
echo "Restarting database bbb_graphql"
|
||||
sudo -u postgres psql -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE datname = 'bbb_graphql'"
|
||||
sudo -u postgres psql -c "drop database if exists bbb_graphql with (force)"
|
||||
sudo -u postgres psql -c "create database bbb_graphql WITH TEMPLATE template0 LC_COLLATE 'C.UTF-8'"
|
||||
sudo -u postgres psql -c "alter database bbb_graphql set timezone to 'UTC'"
|
||||
runuser -u postgres -- psql -q -c "drop database if exists bbb_graphql with (force)"
|
||||
runuser -u postgres -- psql -q -c "create database bbb_graphql WITH TEMPLATE template0 LC_COLLATE 'C.UTF-8'"
|
||||
runuser -u postgres -- psql -q -c "alter database bbb_graphql set timezone to 'UTC'"
|
||||
|
||||
echo "Creating tables in bbb_graphql"
|
||||
sudo -u postgres psql -U postgres -d bbb_graphql -q -f bbb_schema.sql --set ON_ERROR_STOP=on
|
||||
runuser -u postgres -- psql -U postgres -d bbb_graphql -q -f bbb_schema.sql --set ON_ERROR_STOP=on
|
||||
|
||||
echo "Creating frontend in bbb_graphql"
|
||||
DATABASE_FRONTEND_USER="bbb_frontend"
|
||||
@ -40,21 +39,20 @@ fi
|
||||
|
||||
sudo -u postgres psql -q -d bbb_graphql -c "GRANT SELECT ON v_user_connection_auth TO $DATABASE_FRONTEND_USER"
|
||||
|
||||
if [ "$hasura_status" = "active" ]; then
|
||||
echo "Starting Hasura"
|
||||
sudo systemctl start bbb-graphql-server
|
||||
echo "Starting Hasura"
|
||||
sudo systemctl start bbb-graphql-server
|
||||
|
||||
#Check if Hasura is ready before applying metadata
|
||||
HASURA_PORT=8080
|
||||
while ! netstat -tuln | grep ":$HASURA_PORT " > /dev/null; do
|
||||
echo "Waiting for Hasura's port ($HASURA_PORT) to be ready..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
#Check if Hasura is ready before applying metadata
|
||||
HASURA_PORT=8080
|
||||
while ! netstat -tuln | grep ":$HASURA_PORT " > /dev/null; do
|
||||
echo "Waiting for Hasura's port ($HASURA_PORT) to be ready..."
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
if [ "$akka_apps_status" = "active" ]; then
|
||||
echo "Starting Akka-apps"
|
||||
sudo systemctl start bbb-apps-akka
|
||||
fi
|
||||
|
||||
echo "Applying new metadata to Hasura"
|
||||
hasura metadata apply --skip-update-check
|
||||
timeout 15s hasura metadata apply --skip-update-check
|
||||
|
@ -93,7 +93,7 @@ Meteor.startup(() => {
|
||||
</AuthenticatedHandler>
|
||||
</JoinHandler>
|
||||
<UsersAdapter />
|
||||
<ChatAdapter />
|
||||
{/* <ChatAdapter /> */}
|
||||
</>
|
||||
</ContextProviders>,
|
||||
document.getElementById('app'),
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
|
||||
import { UI_DATA_LISTENER_SUBSCRIBED } from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-data-hooks/consts';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
@ -45,17 +47,20 @@ class IntlStartup extends Component {
|
||||
}
|
||||
|
||||
this.fetchLocalizedMessages = this.fetchLocalizedMessages.bind(this);
|
||||
this.sendUiDataToPlugins = this.sendUiDataToPlugins.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { locale, overrideLocaleFromPassedParameter } = this.props;
|
||||
this.fetchLocalizedMessages(overrideLocaleFromPassedParameter || locale, true);
|
||||
window.addEventListener(`${UI_DATA_LISTENER_SUBSCRIBED}-${PluginSdk.IntlLocaleUiDataNames.CURRENT_LOCALE}`, this.sendUiDataToPlugins);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { fetching, messages, normalizedLocale } = this.state;
|
||||
const { locale, overrideLocaleFromPassedParameter } = this.props;
|
||||
|
||||
this.sendUiDataToPlugins();
|
||||
if (overrideLocaleFromPassedParameter !== prevProps.overrideLocaleFromPassedParameter) {
|
||||
this.fetchLocalizedMessages(overrideLocaleFromPassedParameter);
|
||||
} else {
|
||||
@ -64,6 +69,22 @@ class IntlStartup extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener(`${UI_DATA_LISTENER_SUBSCRIBED}-${PluginSdk.IntlLocaleUiDataNames.CURRENT_LOCALE}`, this.sendUiDataToPlugins);
|
||||
}
|
||||
|
||||
sendUiDataToPlugins() {
|
||||
const {
|
||||
locale,
|
||||
} = this.props;
|
||||
window.dispatchEvent(new CustomEvent(PluginSdk.IntlLocaleUiDataNames.CURRENT_LOCALE, {
|
||||
detail: {
|
||||
locale,
|
||||
fallbackLocale: DEFAULT_LANGUAGE,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
fetchLocalizedMessages(locale, init = false) {
|
||||
const url = `./locale?locale=${locale}&init=${init}`;
|
||||
const localesPath = 'locales';
|
||||
@ -185,18 +206,28 @@ class IntlStartup extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const IntlStartupContainer = withTracker(() => {
|
||||
const IntlStartupContainer = (props) => {
|
||||
const setLocalSettings = useUserChangedLocalSettings();
|
||||
|
||||
return (
|
||||
<IntlStartup
|
||||
{...{
|
||||
setLocalSettings,
|
||||
...props,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTracker(() => {
|
||||
const { locale } = Settings.application;
|
||||
const overrideLocaleFromPassedParameter = getFromUserSettings('bbb_override_default_locale', null);
|
||||
const setLocalSettings = useUserChangedLocalSettings();
|
||||
|
||||
return {
|
||||
locale,
|
||||
overrideLocaleFromPassedParameter,
|
||||
setLocalSettings,
|
||||
};
|
||||
})(IntlStartup);
|
||||
|
||||
export default IntlStartupContainer;
|
||||
})(IntlStartupContainer);
|
||||
|
||||
IntlStartup.propTypes = propTypes;
|
||||
IntlStartup.defaultProps = defaultProps;
|
||||
|
@ -17,11 +17,11 @@ import { screenshareHasEnded } from '/imports/ui/components/screenshare/service'
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
|
||||
const propTypes = {
|
||||
amIPresenter: PropTypes.bool.isRequired,
|
||||
amIPresenter: PropTypes.bool,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
amIModerator: PropTypes.bool.isRequired,
|
||||
amIModerator: PropTypes.bool,
|
||||
shortcuts: PropTypes.string,
|
||||
handleTakePresenter: PropTypes.func.isRequired,
|
||||
isTimerActive: PropTypes.bool.isRequired,
|
||||
@ -45,6 +45,8 @@ const propTypes = {
|
||||
const defaultProps = {
|
||||
shortcuts: '',
|
||||
settingsLayout: LAYOUT_TYPE.SMART_LAYOUT,
|
||||
amIPresenter: false,
|
||||
amIModerator: false,
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
|
@ -179,11 +179,16 @@ const propTypes = {
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
emoji: PropTypes.string.isRequired,
|
||||
emoji: PropTypes.string,
|
||||
sidebarContentPanel: PropTypes.string.isRequired,
|
||||
layoutContextDispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
emoji: '',
|
||||
};
|
||||
|
||||
ReactionsButton.propTypes = propTypes;
|
||||
ReactionsButton.defaultProps = defaultProps;
|
||||
|
||||
export default withShortcutHelper(ReactionsButton, ['raiseHand']);
|
||||
|
@ -23,12 +23,16 @@ const { isSafari, isTabletApp } = browserInfo;
|
||||
const propTypes = {
|
||||
intl: PropTypes.objectOf(Object).isRequired,
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
amIPresenter: PropTypes.bool.isRequired,
|
||||
amIPresenter: PropTypes.bool,
|
||||
isScreenBroadcasting: PropTypes.bool.isRequired,
|
||||
isMeteorConnected: PropTypes.bool.isRequired,
|
||||
screenshareDataSavingSetting: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
amIPresenter: false,
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
desktopShareLabel: {
|
||||
id: 'app.actionsBar.actionsDropdown.desktopShareLabel',
|
||||
@ -214,4 +218,5 @@ const ScreenshareButton = ({
|
||||
};
|
||||
|
||||
ScreenshareButton.propTypes = propTypes;
|
||||
ScreenshareButton.defaultProps = defaultProps;
|
||||
export default injectIntl(memo(ScreenshareButton));
|
||||
|
@ -29,6 +29,7 @@ import WebcamContainer from '../webcam/container';
|
||||
import PresentationContainer from '../presentation/container';
|
||||
import ScreenshareContainer from '../screenshare/container';
|
||||
import ExternalVideoPlayerContainer from '../external-video-player/external-video-player-graphql/component';
|
||||
import GenericComponentContainer from '../generic-component-content/container';
|
||||
import EmojiRainContainer from '../emoji-rain/container';
|
||||
import Styled from './styles';
|
||||
import { DEVICE_TYPE, ACTIONS, SMALL_VIEWPORT_BREAKPOINT, PANELS } from '../layout/enums';
|
||||
@ -633,6 +634,13 @@ class App extends Component {
|
||||
<NavBarContainer main="new" />
|
||||
<WebcamContainer isLayoutSwapped={!presentationIsOpen} layoutType={selectedLayout} />
|
||||
<ExternalVideoPlayerContainer />
|
||||
<GenericComponentContainer
|
||||
{...{
|
||||
shouldShowScreenshare,
|
||||
shouldShowSharedNotes,
|
||||
shouldShowExternalVideo,
|
||||
}}
|
||||
/>
|
||||
{shouldShowPresentation ? <PresentationContainer setPresentationFitToWidth={this.setPresentationFitToWidth} fitToWidth={presentationFitToWidth} darkTheme={darkTheme} presentationIsOpen={presentationIsOpen} layoutType={selectedLayout} /> : null}
|
||||
{shouldShowScreenshare ? <ScreenshareContainer isLayoutSwapped={!presentationIsOpen} isPresenter={isPresenter} /> : null}
|
||||
{shouldShowSharedNotes
|
||||
|
@ -80,6 +80,7 @@ const AppContainer = (props) => {
|
||||
} = props;
|
||||
|
||||
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
|
||||
const genericComponent = layoutSelectInput((i) => i.genericComponent);
|
||||
const sidebarNavigation = layoutSelectInput((i) => i.sidebarNavigation);
|
||||
const actionsBarStyle = layoutSelectOutput((i) => i.actionBar);
|
||||
const captionsStyle = layoutSelectOutput((i) => i.captions);
|
||||
@ -188,14 +189,18 @@ const AppContainer = (props) => {
|
||||
|
||||
const shouldShowExternalVideo = isExternalVideoEnabled() && isSharingVideo;
|
||||
|
||||
const shouldShowGenericComponent = genericComponent.hasGenericComponent;
|
||||
|
||||
const validateEnforceLayout = (currentUser) => {
|
||||
const layoutTypes = Object.values(LAYOUT_TYPE);
|
||||
const enforceLayout = currentUser?.enforceLayout;
|
||||
return enforceLayout && layoutTypes.includes(enforceLayout) ? enforceLayout : null;
|
||||
};
|
||||
|
||||
const shouldShowScreenshare = propsShouldShowScreenshare && (viewScreenshare || isPresenter);
|
||||
const shouldShowPresentation = (!shouldShowScreenshare && !shouldShowSharedNotes && !shouldShowExternalVideo
|
||||
const shouldShowScreenshare = propsShouldShowScreenshare
|
||||
&& (viewScreenshare || isPresenter);
|
||||
const shouldShowPresentation = (!shouldShowScreenshare && !shouldShowSharedNotes
|
||||
&& !shouldShowExternalVideo && !shouldShowGenericComponent
|
||||
&& (presentationIsOpen || presentationRestoreOnUpdate)) && isPresentationEnabled();
|
||||
|
||||
return currentUserId
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { Session } from 'meteor/session';
|
||||
@ -23,6 +23,7 @@ import Service from './service';
|
||||
import AudioModalContainer from './audio-modal/container';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import useToggleVoice from './audio-graphql/hooks/useToggleVoice';
|
||||
import { usePreviousValue } from '/imports/ui/components/utils/hooks';
|
||||
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
const KURENTO_CONFIG = Meteor.settings.public.kurento;
|
||||
@ -74,88 +75,87 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
class AudioContainer extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const AudioContainer = (props) => {
|
||||
const {
|
||||
isAudioModalOpen,
|
||||
setAudioModalIsOpen,
|
||||
setVideoPreviewModalIsOpen,
|
||||
isVideoPreviewModalOpen,
|
||||
hasBreakoutRooms,
|
||||
userSelectedMicrophone,
|
||||
userSelectedListenOnly,
|
||||
meetingIsBreakout,
|
||||
init,
|
||||
intl,
|
||||
userLocks,
|
||||
microphoneConstraints,
|
||||
} = props;
|
||||
|
||||
this.init = props.init.bind(this);
|
||||
}
|
||||
const prevProps = usePreviousValue(props);
|
||||
const toggleVoice = useToggleVoice();
|
||||
const { hasBreakoutRooms: hadBreakoutRooms } = prevProps || {};
|
||||
const userIsReturningFromBreakoutRoom = hadBreakoutRooms && !hasBreakoutRooms;
|
||||
|
||||
componentDidMount() {
|
||||
const { meetingIsBreakout } = this.props;
|
||||
|
||||
this.init().then(() => {
|
||||
if (meetingIsBreakout && !Service.isUsingAudio()) {
|
||||
this.joinAudio();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.userIsReturningFromBreakoutRoom(prevProps)) {
|
||||
this.joinAudio();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to determine wheter user is returning from breakout room
|
||||
* to main room.
|
||||
* @param {Object} prevProps prevProps param from componentDidUpdate
|
||||
* @return {boolean} True if user is returning from breakout room
|
||||
* to main room. False, otherwise.
|
||||
*/
|
||||
userIsReturningFromBreakoutRoom(prevProps) {
|
||||
const { hasBreakoutRooms } = this.props;
|
||||
const { hasBreakoutRooms: hadBreakoutRooms } = prevProps;
|
||||
return hadBreakoutRooms && !hasBreakoutRooms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that join (or not) user in audio. If user previously
|
||||
* selected microphone, it will automatically join mic (without audio modal).
|
||||
* If user previously selected listen only option in audio modal, then it will
|
||||
* automatically join listen only.
|
||||
*/
|
||||
joinAudio() {
|
||||
const joinAudio = () => {
|
||||
if (Service.isConnected()) return;
|
||||
|
||||
const {
|
||||
userSelectedMicrophone,
|
||||
userSelectedListenOnly,
|
||||
} = this.props;
|
||||
|
||||
if (userSelectedMicrophone) {
|
||||
joinMicrophone(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (userSelectedListenOnly) joinListenOnly();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
init(toggleVoice).then(() => {
|
||||
if (meetingIsBreakout && !Service.isUsingAudio()) {
|
||||
joinAudio();
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (userIsReturningFromBreakoutRoom) {
|
||||
joinAudio();
|
||||
}
|
||||
}, [userIsReturningFromBreakoutRoom]);
|
||||
|
||||
if (Service.isConnected() && !Service.isListenOnly()) {
|
||||
Service.updateAudioConstraints(microphoneConstraints);
|
||||
|
||||
if (userLocks.userMic && !Service.isMuted()) {
|
||||
Service.toggleMuteMicrophone(toggleVoice);
|
||||
notify(intl.formatMessage(intlMessages.reconectingAsListener), 'info', 'volume_level_2');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isAudioModalOpen, setAudioModalIsOpen,
|
||||
setVideoPreviewModalIsOpen, isVideoPreviewModalOpen } = this.props;
|
||||
return <>
|
||||
{isAudioModalOpen ? <AudioModalContainer
|
||||
{...{
|
||||
priority: "low",
|
||||
setIsOpen: setAudioModalIsOpen,
|
||||
isOpen: isAudioModalOpen
|
||||
}}
|
||||
/> : null}
|
||||
{isVideoPreviewModalOpen ? <VideoPreviewContainer
|
||||
{...{
|
||||
callbackToClose: () => {
|
||||
setVideoPreviewModalIsOpen(false);
|
||||
},
|
||||
priority: "low",
|
||||
setIsOpen: setVideoPreviewModalIsOpen,
|
||||
isOpen: isVideoPreviewModalOpen
|
||||
}}
|
||||
/> : null}
|
||||
</>;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{isAudioModalOpen ? (
|
||||
<AudioModalContainer
|
||||
{...{
|
||||
priority: 'low',
|
||||
setIsOpen: setAudioModalIsOpen,
|
||||
isOpen: isAudioModalOpen,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{isVideoPreviewModalOpen ? (
|
||||
<VideoPreviewContainer
|
||||
{...{
|
||||
callbackToClose: () => {
|
||||
setVideoPreviewModalIsOpen(false);
|
||||
},
|
||||
priority: 'low',
|
||||
setIsOpen: setVideoPreviewModalIsOpen,
|
||||
isOpen: isVideoPreviewModalOpen,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
let didMountAutoJoin = false;
|
||||
|
||||
@ -183,13 +183,14 @@ const messages = {
|
||||
},
|
||||
};
|
||||
|
||||
export default lockContextContainer(injectIntl(withTracker(({ intl, userLocks, isAudioModalOpen, setAudioModalIsOpen,
|
||||
setVideoPreviewModalIsOpen, isVideoPreviewModalOpen }) => {
|
||||
export default lockContextContainer(injectIntl(withTracker(({
|
||||
intl, userLocks, isAudioModalOpen, setAudioModalIsOpen, setVideoPreviewModalIsOpen,
|
||||
}) => {
|
||||
const { microphoneConstraints } = Settings.application;
|
||||
const autoJoin = getFromUserSettings('bbb_auto_join_audio', APP_CONFIG.autoJoin);
|
||||
const enableVideo = getFromUserSettings('bbb_enable_video', KURENTO_CONFIG.enableVideo);
|
||||
const autoShareWebcam = getFromUserSettings('bbb_auto_share_webcam', KURENTO_CONFIG.autoShareWebcam);
|
||||
const { userWebcam, userMic } = userLocks;
|
||||
const { userWebcam } = userLocks;
|
||||
|
||||
const userSelectedMicrophone = didUserSelectedMicrophone();
|
||||
const userSelectedListenOnly = didUserSelectedListenOnly();
|
||||
@ -202,19 +203,9 @@ export default lockContextContainer(injectIntl(withTracker(({ intl, userLocks, i
|
||||
setVideoPreviewModalIsOpen(true);
|
||||
};
|
||||
|
||||
const toggleVoice = useToggleVoice();
|
||||
|
||||
if (Service.isConnected() && !Service.isListenOnly()) {
|
||||
Service.updateAudioConstraints(microphoneConstraints);
|
||||
|
||||
if (userMic && !Service.isMuted()) {
|
||||
Service.toggleMuteMicrophone(toggleVoice);
|
||||
notify(intl.formatMessage(intlMessages.reconectingAsListener), 'info', 'volume_level_2');
|
||||
}
|
||||
}
|
||||
const breakoutUserIsIn = BreakoutsService.getBreakoutUserIsIn(Auth.userID);
|
||||
if(!!breakoutUserIsIn && !meetingIsBreakout) {
|
||||
const userBreakout = Breakouts.find({id: breakoutUserIsIn.id})
|
||||
if (!!breakoutUserIsIn && !meetingIsBreakout) {
|
||||
const userBreakout = Breakouts.find({ id: breakoutUserIsIn.id });
|
||||
userBreakout.observeChanges({
|
||||
removed() {
|
||||
// if the user joined a breakout room, the main room's audio was
|
||||
@ -227,14 +218,14 @@ export default lockContextContainer(injectIntl(withTracker(({ intl, userLocks, i
|
||||
openVideoPreviewModal();
|
||||
}
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
openAudioModal();
|
||||
if (enableVideo && autoShareWebcam) {
|
||||
openVideoPreviewModal();
|
||||
}
|
||||
}, 0);
|
||||
openAudioModal();
|
||||
if (enableVideo && autoShareWebcam) {
|
||||
openVideoPreviewModal();
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -244,12 +235,11 @@ export default lockContextContainer(injectIntl(withTracker(({ intl, userLocks, i
|
||||
meetingIsBreakout,
|
||||
userSelectedMicrophone,
|
||||
userSelectedListenOnly,
|
||||
isAudioModalOpen,
|
||||
isAudioModalOpen,
|
||||
setAudioModalIsOpen,
|
||||
init: async () => {
|
||||
microphoneConstraints,
|
||||
init: async (toggleVoice) => {
|
||||
await Service.init(messages, intl, toggleVoice);
|
||||
const enableVideo = getFromUserSettings('bbb_enable_video', KURENTO_CONFIG.enableVideo);
|
||||
const autoShareWebcam = getFromUserSettings('bbb_auto_share_webcam', KURENTO_CONFIG.autoShareWebcam);
|
||||
if ((!autoJoin || didMountAutoJoin)) {
|
||||
if (enableVideo && autoShareWebcam) {
|
||||
openVideoPreviewModal();
|
||||
@ -258,8 +248,8 @@ export default lockContextContainer(injectIntl(withTracker(({ intl, userLocks, i
|
||||
}
|
||||
Session.set('audioModalIsOpen', true);
|
||||
if (enableVideo && autoShareWebcam) {
|
||||
openAudioModal()
|
||||
openVideoPreviewModal();
|
||||
openAudioModal();
|
||||
openVideoPreviewModal();
|
||||
didMountAutoJoin = true;
|
||||
} else if (!(
|
||||
userSelectedMicrophone
|
||||
@ -273,9 +263,25 @@ export default lockContextContainer(injectIntl(withTracker(({ intl, userLocks, i
|
||||
};
|
||||
})(AudioContainer)));
|
||||
|
||||
AudioContainer.defaultProps = {
|
||||
microphoneConstraints: undefined,
|
||||
};
|
||||
|
||||
AudioContainer.propTypes = {
|
||||
hasBreakoutRooms: PropTypes.bool.isRequired,
|
||||
meetingIsBreakout: PropTypes.bool.isRequired,
|
||||
userSelectedListenOnly: PropTypes.bool.isRequired,
|
||||
userSelectedMicrophone: PropTypes.bool.isRequired,
|
||||
isAudioModalOpen: PropTypes.bool.isRequired,
|
||||
setAudioModalIsOpen: PropTypes.func.isRequired,
|
||||
setVideoPreviewModalIsOpen: PropTypes.func.isRequired,
|
||||
init: PropTypes.func.isRequired,
|
||||
isVideoPreviewModalOpen: PropTypes.bool.isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
userLocks: PropTypes.shape({
|
||||
userMic: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
microphoneConstraints: PropTypes.shape({}),
|
||||
};
|
||||
|
@ -56,7 +56,7 @@ const ManageRoomLabel: React.FC<ManageRoomLabelProps> = ({
|
||||
/>
|
||||
) : (
|
||||
<Styled.AssignBtns
|
||||
random
|
||||
$random
|
||||
data-test="randomlyAssign"
|
||||
label={intl.formatMessage(intlMessages.randomlyAssign)}
|
||||
aria-describedby="randomlyAssignDesc"
|
||||
|
@ -246,7 +246,7 @@ const AssignBtns = styled(Button)`
|
||||
white-space: nowrap;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
${({ random }) => random && `
|
||||
${({ $random }) => $random && `
|
||||
color: ${colorPrimary};
|
||||
`}
|
||||
`;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled, { css, keyframes } from 'styled-components';
|
||||
import {
|
||||
mdPaddingX,
|
||||
smPaddingX,
|
||||
borderSize,
|
||||
listItemBgHover, borderSizeSmall,
|
||||
borderRadius,
|
||||
@ -14,7 +14,7 @@ import {
|
||||
colorWhite,
|
||||
colorGrayLighter,
|
||||
colorGrayLightest,
|
||||
colorBlueLight
|
||||
colorBlueLight,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import {
|
||||
headingsFontWeight,
|
||||
@ -91,7 +91,7 @@ const ellipsis = keyframes`
|
||||
to {
|
||||
width: 1.5em;
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
const ConnectingAnimation = styled.span`
|
||||
&:after {
|
||||
@ -224,7 +224,7 @@ const Panel = styled(ScrollboxVertical)`
|
||||
radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.2), rgba(0,0,0,0)) 0 100%;
|
||||
|
||||
background-color: #fff;
|
||||
padding: ${mdPaddingX};
|
||||
padding: ${smPaddingX};
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
|
@ -1,14 +1,11 @@
|
||||
import styled from 'styled-components';
|
||||
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import {
|
||||
mdPaddingX,
|
||||
mdPaddingY,
|
||||
} from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { smPaddingX } from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
|
||||
|
||||
const Captions = styled.div`
|
||||
background-color: ${colorWhite};
|
||||
padding: ${mdPaddingY} ${mdPaddingY} ${mdPaddingX} ${mdPaddingX};
|
||||
padding: ${smPaddingX};
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
|
@ -1,8 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
smPaddingX,
|
||||
mdPaddingX,
|
||||
mdPaddingY,
|
||||
borderRadius,
|
||||
} from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { ScrollboxVertical } from '/imports/ui/stylesheets/styled-components/scrollable';
|
||||
@ -17,18 +15,7 @@ export const MessageListWrapper = styled.div`
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-left: ${smPaddingX};
|
||||
margin-left: calc(-1 * ${mdPaddingX});
|
||||
padding-right: ${smPaddingX};
|
||||
margin-right: calc(-1 * ${mdPaddingY});
|
||||
padding-bottom: ${mdPaddingX};
|
||||
z-index: 2;
|
||||
[dir='rtl'] & {
|
||||
padding-right: ${mdPaddingX};
|
||||
margin-right: calc(-1 * ${mdPaddingX});
|
||||
padding-left: ${mdPaddingY};
|
||||
margin-left: calc(-1 * ${mdPaddingX});
|
||||
}
|
||||
`;
|
||||
|
||||
export const MessageList = styled(ScrollboxVertical)`
|
||||
|
@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
import { colorWhite, colorPrimary } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
|
||||
import { mdPaddingX } from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { smPaddingX } from '/imports/ui/stylesheets/styled-components/general';
|
||||
|
||||
interface ChatProps {
|
||||
isChrome: boolean;
|
||||
@ -9,7 +9,7 @@ interface ChatProps {
|
||||
|
||||
export const Chat = styled.div<ChatProps>`
|
||||
background-color: ${colorWhite};
|
||||
padding: ${mdPaddingX};
|
||||
padding: ${smPaddingX};
|
||||
padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
@ -77,7 +77,17 @@ class Tooltip extends Component {
|
||||
animation: animations ? DEFAULT_ANIMATION : ANIMATION_NONE,
|
||||
appendTo: document.body,
|
||||
arrow: roundArrow,
|
||||
boundary: 'window',
|
||||
popperOptions: {
|
||||
modifiers: [
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
altAxis: true,
|
||||
boundary: document.documentElement,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
content: title,
|
||||
delay: overrideDelay,
|
||||
duration: animations ? ANIMATION_DURATION : 0,
|
||||
@ -89,7 +99,6 @@ class Tooltip extends Component {
|
||||
placement: overridePlacement,
|
||||
touch: ['hold', 1000],
|
||||
theme: 'bbbtip',
|
||||
multiple: false,
|
||||
};
|
||||
this.tooltip = Tippy(`#${this.tippySelectorId}`, options);
|
||||
}
|
||||
|
@ -0,0 +1,63 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { GenericComponent } from 'bigbluebutton-html-plugin-sdk';
|
||||
import * as Styled from './styles';
|
||||
import { GenericComponentProps } from './types';
|
||||
import { EXTERNAL_VIDEO_STOP } from '../external-video-player/mutations';
|
||||
import NotesService from '/imports/ui/components/notes/service';
|
||||
import GenericComponentItem from './generic-component-item/component';
|
||||
import { screenshareHasEnded } from '../screenshare/service';
|
||||
|
||||
const mapGenericComponentItems = (
|
||||
genericComponents: GenericComponent[],
|
||||
) => genericComponents.map((genericComponent) => (
|
||||
<GenericComponentItem
|
||||
key={genericComponent.id}
|
||||
renderFunction={genericComponent.contentFunction}
|
||||
/>
|
||||
));
|
||||
|
||||
const GenericComponentContent: React.FC<GenericComponentProps> = ({
|
||||
isResizing,
|
||||
genericComponent,
|
||||
renderFunctionComponents,
|
||||
hasExternalVideoOnLayout,
|
||||
isSharedNotesPinned,
|
||||
hasScreenShareOnLayout,
|
||||
}) => {
|
||||
const [stopExternalVideoShare] = useMutation(EXTERNAL_VIDEO_STOP);
|
||||
|
||||
const {
|
||||
height,
|
||||
width,
|
||||
top,
|
||||
left,
|
||||
right,
|
||||
} = genericComponent;
|
||||
|
||||
const isMinimized = width === 0 && height === 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (hasExternalVideoOnLayout) stopExternalVideoShare();
|
||||
if (isSharedNotesPinned) NotesService.pinSharedNotes(false);
|
||||
if (hasScreenShareOnLayout) screenshareHasEnded();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Styled.Container
|
||||
style={{
|
||||
height,
|
||||
width,
|
||||
top,
|
||||
left,
|
||||
right,
|
||||
}}
|
||||
isResizing={isResizing}
|
||||
isMinimized={isMinimized}
|
||||
>
|
||||
{mapGenericComponentItems(renderFunctionComponents)}
|
||||
</Styled.Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenericComponentContent;
|
@ -0,0 +1,71 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
|
||||
|
||||
import {
|
||||
layoutDispatch,
|
||||
layoutSelectInput,
|
||||
layoutSelectOutput,
|
||||
} from '../layout/context';
|
||||
import {
|
||||
GenericComponent as GenericComponentFromLayout,
|
||||
Input,
|
||||
Output,
|
||||
} from '../layout/layoutTypes';
|
||||
import { PluginsContext } from '../components-data/plugin-context/context';
|
||||
import { GenericComponentContainerProps } from './types';
|
||||
import { ACTIONS } from '../layout/enums';
|
||||
import GenericComponentContent from './component';
|
||||
|
||||
const GenericComponentContainer: React.FC<GenericComponentContainerProps> = (props: GenericComponentContainerProps) => {
|
||||
const {
|
||||
shouldShowScreenshare,
|
||||
shouldShowSharedNotes,
|
||||
shouldShowExternalVideo,
|
||||
} = props;
|
||||
|
||||
const hasExternalVideoOnLayout: boolean = layoutSelectInput((i: Input) => i.externalVideo.hasExternalVideo);
|
||||
const hasScreenShareOnLayout: boolean = layoutSelectInput((i: Input) => i.screenShare.hasScreenShare);
|
||||
const isSharedNotesPinned: boolean = layoutSelectInput((i: Input) => i.sharedNotes.isPinned);
|
||||
|
||||
const {
|
||||
pluginsExtensibleAreasAggregatedState,
|
||||
} = useContext(PluginsContext);
|
||||
let genericComponentExtensibleArea = [] as PluginSdk.GenericComponent[];
|
||||
if (pluginsExtensibleAreasAggregatedState.genericComponents) {
|
||||
genericComponentExtensibleArea = [
|
||||
...pluginsExtensibleAreasAggregatedState.genericComponents as PluginSdk.GenericComponent[],
|
||||
];
|
||||
}
|
||||
useEffect(() => {
|
||||
if (shouldShowScreenshare || shouldShowSharedNotes || shouldShowExternalVideo) {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_GENERIC_COMPONENT,
|
||||
value: false,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
shouldShowScreenshare,
|
||||
shouldShowSharedNotes,
|
||||
shouldShowExternalVideo,
|
||||
]);
|
||||
|
||||
const genericComponent: GenericComponentFromLayout = layoutSelectOutput((i: Output) => i.genericComponent);
|
||||
const hasGenericComponentOnLayout: boolean = layoutSelectInput((i: Input) => i.genericComponent.hasGenericComponent);
|
||||
const cameraDock = layoutSelectInput((i: Input) => i.cameraDock);
|
||||
const { isResizing } = cameraDock;
|
||||
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
if (!hasGenericComponentOnLayout || !genericComponentExtensibleArea) return null;
|
||||
return (
|
||||
<GenericComponentContent
|
||||
hasExternalVideoOnLayout={hasExternalVideoOnLayout}
|
||||
isSharedNotesPinned={isSharedNotesPinned}
|
||||
hasScreenShareOnLayout={hasScreenShareOnLayout}
|
||||
renderFunctionComponents={genericComponentExtensibleArea}
|
||||
isResizing={isResizing}
|
||||
genericComponent={genericComponent}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenericComponentContainer;
|
@ -0,0 +1,26 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { GenericComponentItemProps } from './types';
|
||||
|
||||
const GenericComponentItem: React.FC<GenericComponentItemProps> = (props) => {
|
||||
const {
|
||||
renderFunction,
|
||||
} = props;
|
||||
const elementRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (elementRef.current && renderFunction) {
|
||||
renderFunction(elementRef.current);
|
||||
}
|
||||
}, [elementRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
ref={elementRef}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenericComponentItem;
|
@ -0,0 +1,3 @@
|
||||
export interface GenericComponentItemProps {
|
||||
renderFunction: (element: HTMLElement) => void;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
type ContainerProps = {
|
||||
isResizing: boolean;
|
||||
isMinimized: boolean;
|
||||
};
|
||||
|
||||
export const Container = styled.div<ContainerProps>`
|
||||
position: absolute;
|
||||
pointer-events: inherit;
|
||||
background: var(--color-black);
|
||||
z-index: 5;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
${({ isResizing }) => isResizing && `
|
||||
pointer-events: none;
|
||||
`}
|
||||
${({ isMinimized }) => isMinimized && `
|
||||
display: none;
|
||||
`}
|
||||
`;
|
||||
|
||||
export default {
|
||||
Container,
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import { GenericComponent } from 'bigbluebutton-html-plugin-sdk';
|
||||
import { GenericComponent as GenericComponentLayout } from '../layout/layoutTypes';
|
||||
|
||||
export interface GenericComponentContainerProps {
|
||||
shouldShowScreenshare: boolean ;
|
||||
shouldShowSharedNotes: boolean ;
|
||||
shouldShowExternalVideo: boolean ;
|
||||
}
|
||||
|
||||
export interface GenericComponentProps {
|
||||
isResizing: boolean;
|
||||
genericComponent: GenericComponentLayout;
|
||||
renderFunctionComponents: GenericComponent[];
|
||||
hasExternalVideoOnLayout: boolean;
|
||||
isSharedNotesPinned: boolean;
|
||||
hasScreenShareOnLayout: boolean;
|
||||
}
|
@ -1185,6 +1185,56 @@ const reducer = (state, action) => {
|
||||
};
|
||||
}
|
||||
|
||||
// GENERIC COMPONENT
|
||||
case ACTIONS.SET_HAS_GENERIC_COMPONENT: {
|
||||
const { genericComponent } = state.input;
|
||||
if (genericComponent.hasGenericComponent === action.value) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
input: {
|
||||
...state.input,
|
||||
genericComponent: {
|
||||
...genericComponent,
|
||||
hasGenericComponent: action.value,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case ACTIONS.SET_GENERIC_COMPONENT_OUTPUT: {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
top,
|
||||
left,
|
||||
right,
|
||||
} = action.value;
|
||||
const { genericComponent } = state.output;
|
||||
if (genericComponent.width === width
|
||||
&& genericComponent.height === height
|
||||
&& genericComponent.top === top
|
||||
&& genericComponent.left === left
|
||||
&& genericComponent.right === right) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
output: {
|
||||
...state.output,
|
||||
genericComponent: {
|
||||
...genericComponent,
|
||||
width,
|
||||
height,
|
||||
top,
|
||||
left,
|
||||
right,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// NOTES
|
||||
case ACTIONS.SET_SHARED_NOTES_OUTPUT: {
|
||||
const {
|
||||
|
@ -107,6 +107,9 @@ export const ACTIONS = {
|
||||
SET_EXTERNAL_VIDEO_SIZE: 'setExternalVideoSize',
|
||||
SET_EXTERNAL_VIDEO_OUTPUT: 'setExternalVideoOutput',
|
||||
|
||||
SET_HAS_GENERIC_COMPONENT: 'setHasGenericComponent',
|
||||
SET_GENERIC_COMPONENT_OUTPUT: 'setGenericComponentOutput',
|
||||
|
||||
SET_SHARED_NOTES_OUTPUT: 'setSharedNotesOutput',
|
||||
SET_NOTES_IS_PINNED: 'setNotesIsPinned',
|
||||
};
|
||||
|
@ -95,6 +95,13 @@ export const INITIAL_INPUT_STATE = {
|
||||
browserWidth: 0,
|
||||
browserHeight: 0,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: false,
|
||||
width: 0,
|
||||
height: 0,
|
||||
browserWidth: 0,
|
||||
browserHeight: 0,
|
||||
},
|
||||
sharedNotes: {
|
||||
isPinned: false,
|
||||
width: 0,
|
||||
@ -234,6 +241,15 @@ export const INITIAL_OUTPUT_STATE = {
|
||||
tabOrder: 0,
|
||||
zIndex: 1,
|
||||
},
|
||||
genericComponent: {
|
||||
display: false,
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
tabOrder: 0,
|
||||
zIndex: 1,
|
||||
},
|
||||
sharedNotes: {
|
||||
display: false,
|
||||
width: 0,
|
||||
|
@ -296,6 +296,18 @@ const CamerasOnlyLayout = (props) => {
|
||||
},
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_GENERIC_COMPONENT_OUTPUT,
|
||||
value: {
|
||||
display: false,
|
||||
width: mediaBounds.width,
|
||||
height: mediaBounds.height,
|
||||
top: mediaBounds.top,
|
||||
left: mediaBounds.left,
|
||||
right: isRTL ? mediaBounds.right : null,
|
||||
},
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SHARED_NOTES_OUTPUT,
|
||||
value: {
|
||||
@ -353,6 +365,9 @@ const CamerasOnlyLayout = (props) => {
|
||||
externalVideo: {
|
||||
hasExternalVideo: false,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: false,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: false,
|
||||
},
|
||||
|
@ -33,6 +33,7 @@ const CustomLayout = (props) => {
|
||||
|
||||
const presentationInput = layoutSelectInput((i) => i.presentation);
|
||||
const externalVideoInput = layoutSelectInput((i) => i.externalVideo);
|
||||
const genericComponentInput = layoutSelectInput((i) => i.genericComponent);
|
||||
const screenShareInput = layoutSelectInput((i) => i.screenShare);
|
||||
const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
|
||||
|
||||
@ -163,6 +164,9 @@ const CustomLayout = (props) => {
|
||||
externalVideo: {
|
||||
hasExternalVideo: input.externalVideo.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: input.genericComponent.hasGenericComponent
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: input.screenShare.hasScreenShare,
|
||||
width: input.screenShare.width,
|
||||
@ -204,6 +208,9 @@ const CustomLayout = (props) => {
|
||||
externalVideo: {
|
||||
hasExternalVideo: input.externalVideo.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: input.genericComponent.hasGenericComponent
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: input.screenShare.hasScreenShare,
|
||||
width: input.screenShare.width,
|
||||
@ -224,12 +231,14 @@ const CustomLayout = (props) => {
|
||||
const calculatesSidebarContentHeight = (cameraDockHeight) => {
|
||||
const { isOpen, slidesLength } = presentationInput;
|
||||
const { hasExternalVideo } = externalVideoInput;
|
||||
const { hasGenericComponent } = genericComponentInput;
|
||||
const { hasScreenShare } = screenShareInput;
|
||||
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
|
||||
|
||||
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
|
||||
const isGeneralMediaOff =
|
||||
!hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
|
||||
!hasPresentation && !hasExternalVideo
|
||||
&& !hasScreenShare && !isSharedNotesPinned && !hasGenericComponent;
|
||||
|
||||
let sidebarContentHeight = 0;
|
||||
if (sidebarContentInput.isOpen) {
|
||||
@ -401,6 +410,7 @@ const CustomLayout = (props) => {
|
||||
const calculatesMediaBounds = (sidebarNavWidth, sidebarContentWidth, cameraDockBounds) => {
|
||||
const { isOpen, slidesLength } = presentationInput;
|
||||
const { hasExternalVideo } = externalVideoInput;
|
||||
const { hasGenericComponent } = genericComponentInput;
|
||||
const { hasScreenShare } = screenShareInput;
|
||||
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
|
||||
|
||||
@ -415,7 +425,8 @@ const CustomLayout = (props) => {
|
||||
|
||||
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
|
||||
const isGeneralMediaOff =
|
||||
!hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
|
||||
!hasPresentation && !hasExternalVideo &&
|
||||
!hasScreenShare && !isSharedNotesPinned && !hasGenericComponent;
|
||||
|
||||
if (!isOpen || isGeneralMediaOff) {
|
||||
mediaBounds.width = 0;
|
||||
@ -430,7 +441,8 @@ const CustomLayout = (props) => {
|
||||
if (
|
||||
fullscreenElement === 'Presentation' ||
|
||||
fullscreenElement === 'Screenshare' ||
|
||||
fullscreenElement === 'ExternalVideo'
|
||||
fullscreenElement === 'ExternalVideo' ||
|
||||
fullscreenElement === 'GenericComponent'
|
||||
) {
|
||||
mediaBounds.width = windowWidth();
|
||||
mediaBounds.height = windowHeight();
|
||||
@ -736,6 +748,17 @@ const CustomLayout = (props) => {
|
||||
right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
|
||||
},
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_GENERIC_COMPONENT_OUTPUT,
|
||||
value: {
|
||||
width: mediaBounds.width,
|
||||
height: mediaBounds.height,
|
||||
top: mediaBounds.top,
|
||||
left: mediaBounds.left,
|
||||
right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
|
||||
},
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SHARED_NOTES_OUTPUT,
|
||||
|
@ -27,6 +27,7 @@ const LayoutEngine = ({ layoutType }) => {
|
||||
const sidebarNavigationInput = layoutSelectInput((i) => i.sidebarNavigation);
|
||||
const sidebarContentInput = layoutSelectInput((i) => i.sidebarContent);
|
||||
const externalVideoInput = layoutSelectInput((i) => i.externalVideo);
|
||||
const genericComponentInput = layoutSelectInput((i) => i.genericComponent);
|
||||
const screenShareInput = layoutSelectInput((i) => i.screenShare);
|
||||
const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
|
||||
|
||||
@ -54,6 +55,7 @@ const LayoutEngine = ({ layoutType }) => {
|
||||
const baseCameraDockBounds = (mediaAreaBounds, sidebarSize) => {
|
||||
const { isOpen, slidesLength } = presentationInput;
|
||||
const { hasExternalVideo } = externalVideoInput;
|
||||
const { hasGenericComponent } = genericComponentInput;
|
||||
const { hasScreenShare } = screenShareInput;
|
||||
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
|
||||
|
||||
@ -69,7 +71,8 @@ const LayoutEngine = ({ layoutType }) => {
|
||||
const navBarHeight = calculatesNavbarHeight();
|
||||
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
|
||||
const isGeneralMediaOff = !hasPresentation
|
||||
&& !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
|
||||
&& !hasExternalVideo && !hasScreenShare
|
||||
&& !isSharedNotesPinned && !hasGenericComponent;
|
||||
|
||||
if (!isOpen || isGeneralMediaOff) {
|
||||
cameraDockBounds.width = mediaAreaBounds.width;
|
||||
|
@ -111,6 +111,7 @@ const ParticipantsAndChatOnlyLayout = (props) => {
|
||||
fullscreenElement === 'Presentation'
|
||||
|| fullscreenElement === 'Screenshare'
|
||||
|| fullscreenElement === 'ExternalVideo'
|
||||
|| fullscreenElement === 'GenericComponent'
|
||||
) {
|
||||
mediaBounds.width = windowWidth();
|
||||
mediaBounds.height = windowHeight();
|
||||
@ -334,6 +335,18 @@ const ParticipantsAndChatOnlyLayout = (props) => {
|
||||
},
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_GENERIC_COMPONENT_OUTPUT,
|
||||
value: {
|
||||
display: false,
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: mediaBounds.top,
|
||||
left: mediaBounds.left,
|
||||
right: mediaBounds.right,
|
||||
},
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SHARED_NOTES_OUTPUT,
|
||||
value: {
|
||||
@ -396,6 +409,11 @@ const ParticipantsAndChatOnlyLayout = (props) => {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: false,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: false,
|
||||
width: 0,
|
||||
|
@ -36,6 +36,7 @@ const PresentationFocusLayout = (props) => {
|
||||
|
||||
const presentationInput = layoutSelectInput((i) => i.presentation);
|
||||
const externalVideoInput = layoutSelectInput((i) => i.externalVideo);
|
||||
const genericComponentInput = layoutSelectInput((i) => i.genericComponent);
|
||||
const screenShareInput = layoutSelectInput((i) => i.screenShare);
|
||||
const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
|
||||
|
||||
@ -106,6 +107,9 @@ const PresentationFocusLayout = (props) => {
|
||||
externalVideo: {
|
||||
hasExternalVideo: input.externalVideo.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: input.genericComponent.hasGenericComponent,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: input.screenShare.hasScreenShare,
|
||||
width: input.screenShare.width,
|
||||
@ -144,6 +148,9 @@ const PresentationFocusLayout = (props) => {
|
||||
externalVideo: {
|
||||
hasExternalVideo: input.externalVideo.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: input.genericComponent.hasGenericComponent,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: input.screenShare.hasScreenShare,
|
||||
width: input.screenShare.width,
|
||||
@ -161,12 +168,14 @@ const PresentationFocusLayout = (props) => {
|
||||
const calculatesSidebarContentHeight = () => {
|
||||
const { isOpen, slidesLength } = presentationInput;
|
||||
const { hasExternalVideo } = externalVideoInput;
|
||||
const { hasGenericComponent } = genericComponentInput;
|
||||
const { hasScreenShare } = screenShareInput;
|
||||
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
|
||||
|
||||
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
|
||||
const isGeneralMediaOff =
|
||||
!hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
|
||||
!hasPresentation && !hasExternalVideo &&
|
||||
!hasScreenShare && !isSharedNotesPinned && !hasGenericComponent;
|
||||
|
||||
const { navBarHeight, sidebarContentMinHeight } = DEFAULT_VALUES;
|
||||
let height = 0;
|
||||
@ -267,7 +276,8 @@ const PresentationFocusLayout = (props) => {
|
||||
if (
|
||||
fullscreenElement === 'Presentation' ||
|
||||
fullscreenElement === 'Screenshare' ||
|
||||
fullscreenElement === 'ExternalVideo'
|
||||
fullscreenElement === 'ExternalVideo' ||
|
||||
fullscreenElement === 'GenericComponent'
|
||||
) {
|
||||
mediaBounds.width = windowWidth();
|
||||
mediaBounds.height = windowHeight();
|
||||
@ -496,6 +506,17 @@ const PresentationFocusLayout = (props) => {
|
||||
},
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_GENERIC_COMPONENT_OUTPUT,
|
||||
value: {
|
||||
width: isOpen ? mediaBounds.width : 0,
|
||||
height: isOpen ? mediaBounds.height : 0,
|
||||
top: mediaBounds.top,
|
||||
left: mediaBounds.left,
|
||||
right: mediaBounds.right,
|
||||
},
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SHARED_NOTES_OUTPUT,
|
||||
value: {
|
||||
|
@ -47,6 +47,7 @@ const PresentationOnlyLayout = (props) => {
|
||||
fullscreenElement === 'Presentation'
|
||||
|| fullscreenElement === 'Screenshare'
|
||||
|| fullscreenElement === 'ExternalVideo'
|
||||
|| fullscreenElement === 'GenericComponent'
|
||||
) {
|
||||
mediaBounds.width = windowWidth();
|
||||
mediaBounds.height = windowHeight();
|
||||
@ -287,6 +288,17 @@ const PresentationOnlyLayout = (props) => {
|
||||
},
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_GENERIC_COMPONENT_OUTPUT,
|
||||
value: {
|
||||
width: isOpen ? mediaBounds.width : 0,
|
||||
height: isOpen ? mediaBounds.height : 0,
|
||||
top: mediaBounds.top,
|
||||
left: mediaBounds.left,
|
||||
right: mediaBounds.right,
|
||||
},
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SHARED_NOTES_OUTPUT,
|
||||
value: {
|
||||
@ -345,6 +357,9 @@ const PresentationOnlyLayout = (props) => {
|
||||
externalVideo: {
|
||||
hasExternalVideo: input.externalVideo.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: input.genericComponent.hasGenericComponent,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: input.screenShare.hasScreenShare,
|
||||
width: input.screenShare.width,
|
||||
|
@ -35,6 +35,7 @@ const SmartLayout = (props) => {
|
||||
const actionbarInput = layoutSelectInput((i) => i.actionBar);
|
||||
const navbarInput = layoutSelectInput((i) => i.navBar);
|
||||
const externalVideoInput = layoutSelectInput((i) => i.externalVideo);
|
||||
const genericComponentInput = layoutSelectInput((i) => i.genericComponent);
|
||||
const screenShareInput = layoutSelectInput((i) => i.screenShare);
|
||||
const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
@ -102,6 +103,9 @@ const SmartLayout = (props) => {
|
||||
externalVideo: {
|
||||
hasExternalVideo: externalVideoInput.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: genericComponentInput.hasGenericComponent,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: screenShareInput.hasScreenShare,
|
||||
width: screenShareInput.width,
|
||||
@ -143,6 +147,9 @@ const SmartLayout = (props) => {
|
||||
externalVideo: {
|
||||
hasExternalVideo: externalVideoInput.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: genericComponentInput.hasGenericComponent,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: screenShareInput.hasScreenShare,
|
||||
width: screenShareInput.width,
|
||||
@ -281,12 +288,14 @@ const SmartLayout = (props) => {
|
||||
const calculatesMediaBounds = (mediaAreaBounds, slideSize, sidebarSize, screenShareSize) => {
|
||||
const { isOpen, slidesLength } = presentationInput;
|
||||
const { hasExternalVideo } = externalVideoInput;
|
||||
const { hasGenericComponent } = genericComponentInput;
|
||||
const { hasScreenShare } = screenShareInput;
|
||||
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
|
||||
|
||||
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
|
||||
const isGeneralMediaOff =
|
||||
!hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
|
||||
!hasPresentation && !hasExternalVideo &&
|
||||
!hasScreenShare && !isSharedNotesPinned && !hasGenericComponent;
|
||||
|
||||
const mediaBounds = {};
|
||||
const { element: fullscreenElement } = fullscreen;
|
||||
@ -304,7 +313,8 @@ const SmartLayout = (props) => {
|
||||
if (
|
||||
fullscreenElement === 'Presentation' ||
|
||||
fullscreenElement === 'Screenshare' ||
|
||||
fullscreenElement === 'ExternalVideo'
|
||||
fullscreenElement === 'ExternalVideo' ||
|
||||
fullscreenElement === 'GenericComponent'
|
||||
) {
|
||||
mediaBounds.width = windowWidth();
|
||||
mediaBounds.height = windowHeight();
|
||||
@ -318,7 +328,8 @@ const SmartLayout = (props) => {
|
||||
const mediaContentSize = hasScreenShare ? screenShareSize : slideSize;
|
||||
|
||||
if (cameraDockInput.numCameras > 0 && !cameraDockInput.isDragging) {
|
||||
if (mediaContentSize.width !== 0 && mediaContentSize.height !== 0 && !hasExternalVideo) {
|
||||
if (mediaContentSize.width !== 0 && mediaContentSize.height !== 0
|
||||
&& !hasExternalVideo && !hasGenericComponent) {
|
||||
if (mediaContentSize.width < mediaAreaBounds.width && !isMobile) {
|
||||
if (mediaContentSize.width < mediaAreaBounds.width * 0.8) {
|
||||
mediaBounds.width = mediaContentSize.width;
|
||||
@ -567,6 +578,17 @@ const SmartLayout = (props) => {
|
||||
right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
|
||||
},
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_GENERIC_COMPONENT_OUTPUT,
|
||||
value: {
|
||||
width: mediaBounds.width,
|
||||
height: mediaBounds.height,
|
||||
top: mediaBounds.top,
|
||||
left: mediaBounds.left,
|
||||
right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
|
||||
},
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SHARED_NOTES_OUTPUT,
|
||||
|
@ -35,6 +35,7 @@ const VideoFocusLayout = (props) => {
|
||||
|
||||
const presentationInput = layoutSelectInput((i) => i.presentation);
|
||||
const externalVideoInput = layoutSelectInput((i) => i.externalVideo);
|
||||
const genericComponentInput = layoutSelectInput((i) => i.genericComponent);
|
||||
const screenShareInput = layoutSelectInput((i) => i.screenShare);
|
||||
const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
|
||||
|
||||
@ -109,6 +110,9 @@ const VideoFocusLayout = (props) => {
|
||||
externalVideo: {
|
||||
hasExternalVideo: input.externalVideo.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: input.genericComponent.hasGenericComponent,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: input.screenShare.hasScreenShare,
|
||||
width: input.screenShare.width,
|
||||
@ -147,6 +151,9 @@ const VideoFocusLayout = (props) => {
|
||||
externalVideo: {
|
||||
hasExternalVideo: input.externalVideo.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: input.genericComponent.hasGenericComponent,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: input.screenShare.hasScreenShare,
|
||||
width: input.screenShare.width,
|
||||
@ -164,13 +171,15 @@ const VideoFocusLayout = (props) => {
|
||||
const calculatesSidebarContentHeight = () => {
|
||||
const { isOpen, slidesLength } = presentationInput;
|
||||
const { hasExternalVideo } = externalVideoInput;
|
||||
const { hasGenericComponent } = genericComponentInput;
|
||||
const { hasScreenShare } = screenShareInput;
|
||||
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
|
||||
|
||||
const navBarHeight = calculatesNavbarHeight();
|
||||
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
|
||||
const isGeneralMediaOff =
|
||||
!hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
|
||||
!hasPresentation && !hasExternalVideo &&
|
||||
!hasScreenShare && !isSharedNotesPinned && !hasGenericComponent;
|
||||
|
||||
let minHeight = 0;
|
||||
let height = 0;
|
||||
@ -262,7 +271,8 @@ const VideoFocusLayout = (props) => {
|
||||
if (
|
||||
fullscreenElement === 'Presentation' ||
|
||||
fullscreenElement === 'Screenshare' ||
|
||||
fullscreenElement === 'ExternalVideo'
|
||||
fullscreenElement === 'ExternalVideo' ||
|
||||
fullscreenElement === 'GenericComponent'
|
||||
) {
|
||||
mediaBounds.width = windowWidth();
|
||||
mediaBounds.height = windowHeight();
|
||||
@ -507,6 +517,17 @@ const VideoFocusLayout = (props) => {
|
||||
},
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_GENERIC_COMPONENT_OUTPUT,
|
||||
value: {
|
||||
width: mediaBounds.width,
|
||||
height: mediaBounds.height,
|
||||
top: mediaBounds.top,
|
||||
left: mediaBounds.left,
|
||||
right: isRTL ? mediaBounds.right : null,
|
||||
},
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SHARED_NOTES_OUTPUT,
|
||||
value: {
|
||||
|
@ -63,6 +63,20 @@ export interface ExternalVideo {
|
||||
zIndex?: number;
|
||||
right?: number;
|
||||
}
|
||||
|
||||
export interface GenericComponent {
|
||||
hasGenericComponent?: boolean;
|
||||
browserHeight?: number;
|
||||
browserWidth?: number;
|
||||
height: number;
|
||||
width: number;
|
||||
display?: boolean;
|
||||
left?: number;
|
||||
tabOrder?: number;
|
||||
top?: number;
|
||||
zIndex?: number;
|
||||
right?: number;
|
||||
}
|
||||
interface NavBar {
|
||||
hasNavBar?: boolean;
|
||||
height: number;
|
||||
@ -215,7 +229,8 @@ interface Input {
|
||||
browser: Browser;
|
||||
cameraDock: CameraDock
|
||||
customParameters: NonNullable<unknown>;
|
||||
externalVideo: ExternalVideo
|
||||
externalVideo: ExternalVideo;
|
||||
genericComponent: GenericComponent;
|
||||
navBar: NavBar;
|
||||
notificationsBar: NotificationsBar;
|
||||
presentation: Presentation;
|
||||
@ -228,17 +243,18 @@ interface Input {
|
||||
|
||||
interface Output {
|
||||
actionBar: ActionBar;
|
||||
cameraDock: CameraDock;
|
||||
captions: Captions;
|
||||
dropZoneAreas: DropzoneAreas;
|
||||
externalVideo: ExternalVideo;
|
||||
mediaArea: Size;
|
||||
navBar: NavBar;
|
||||
presentation: Presentation;
|
||||
screenShare: ScreenShare;
|
||||
sharedNotes: SharedNotes;
|
||||
sidebarContent: SidebarContent;
|
||||
sidebarNavigation: SidebarNavigation;
|
||||
cameraDock: CameraDock;
|
||||
captions: Captions;
|
||||
dropZoneAreas: DropzoneAreas;
|
||||
externalVideo: ExternalVideo;
|
||||
genericComponent: GenericComponent;
|
||||
mediaArea: Size;
|
||||
navBar: NavBar;
|
||||
presentation: Presentation;
|
||||
screenShare: ScreenShare;
|
||||
sharedNotes: SharedNotes;
|
||||
sidebarContent: SidebarContent;
|
||||
sidebarNavigation: SidebarNavigation;
|
||||
}
|
||||
|
||||
interface Layout {
|
||||
|
@ -172,9 +172,8 @@ const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
isModerator: PropTypes.bool.isRequired,
|
||||
isModerator: PropTypes.bool,
|
||||
isPresenter: PropTypes.bool.isRequired,
|
||||
showToggleLabel: PropTypes.bool.isRequired,
|
||||
application: PropTypes.shape({
|
||||
selectedLayout: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
@ -182,6 +181,11 @@ const propTypes = {
|
||||
setLocalSettings: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
isModerator: false,
|
||||
};
|
||||
|
||||
LayoutModalComponent.propTypes = propTypes;
|
||||
LayoutModalComponent.defaultProps = defaultProps;
|
||||
|
||||
export default injectIntl(LayoutModalComponent);
|
||||
|
@ -15,20 +15,10 @@ import { layoutSelectInput, layoutSelectOutput, layoutDispatch } from '../layout
|
||||
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
|
||||
import { PANELS } from '/imports/ui/components/layout/enums';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import useChat from '/imports/ui/core/hooks/useChat';
|
||||
|
||||
const PUBLIC_CONFIG = Meteor.settings.public;
|
||||
|
||||
const checkUnreadMessages = ({
|
||||
groupChatsMessages, groupChats, users, idChatOpen,
|
||||
}) => {
|
||||
const activeChats = userListService.getActiveChats({ groupChatsMessages, groupChats, users });
|
||||
const hasUnreadMessages = activeChats
|
||||
.filter((chat) => chat.userId !== idChatOpen)
|
||||
.some((chat) => chat.unreadCounter > 0);
|
||||
|
||||
return hasUnreadMessages;
|
||||
};
|
||||
|
||||
const NavBarContainer = ({ children, ...props }) => {
|
||||
const usingChatContext = useContext(ChatContext);
|
||||
const usingUsersContext = useContext(UsersContext);
|
||||
@ -51,9 +41,12 @@ const NavBarContainer = ({ children, ...props }) => {
|
||||
const { sidebarNavPanel } = sidebarNavigation;
|
||||
|
||||
const hasUnreadNotes = sidebarContentPanel !== PANELS.SHARED_NOTES && unread && !notesIsPinned;
|
||||
const hasUnreadMessages = checkUnreadMessages(
|
||||
{ groupChatsMessages, groupChats, users: users[Auth.meetingID] },
|
||||
);
|
||||
|
||||
const { data: chats } = useChat((chat) => ({
|
||||
totalUnread: chat.totalUnread,
|
||||
}));
|
||||
|
||||
const hasUnreadMessages = chats && chats.reduce((acc, chat) => acc + chat?.totalUnread, 0) > 0;
|
||||
|
||||
const { data: currentUserData } = useCurrentUser((user) => ({
|
||||
isModerator: user.isModerator,
|
||||
|
@ -109,7 +109,7 @@ const propTypes = {
|
||||
isBreakoutRoom: PropTypes.bool,
|
||||
isMeteorConnected: PropTypes.bool.isRequired,
|
||||
isDropdownOpen: PropTypes.bool,
|
||||
audioCaptionsEnabled: PropTypes.bool.isRequired,
|
||||
audioCaptionsEnabled: PropTypes.bool,
|
||||
audioCaptionsActive: PropTypes.bool.isRequired,
|
||||
audioCaptionsSet: PropTypes.func.isRequired,
|
||||
isMobile: PropTypes.bool.isRequired,
|
||||
@ -126,6 +126,7 @@ const defaultProps = {
|
||||
shortcuts: '',
|
||||
isBreakoutRoom: false,
|
||||
isDropdownOpen: false,
|
||||
audioCaptionsEnabled: false,
|
||||
};
|
||||
|
||||
const ALLOW_FULLSCREEN = Meteor.settings.public.app.allowFullscreen;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
mdPaddingX,
|
||||
smPaddingX,
|
||||
} from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
|
||||
@ -8,7 +8,7 @@ import CommonHeader from '/imports/ui/components/common/control-header/component
|
||||
|
||||
const Notes = styled.div`
|
||||
background-color: ${colorWhite};
|
||||
padding: ${mdPaddingX};
|
||||
padding: ${smPaddingX};
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
|
@ -0,0 +1,54 @@
|
||||
import { useEffect, useState, useContext } from 'react';
|
||||
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
|
||||
|
||||
import {
|
||||
ExtensibleAreaComponentManagerProps, ExtensibleArea,
|
||||
ExtensibleAreaComponentManager,
|
||||
} from '../../types';
|
||||
import { PluginsContext } from '../../../../components-data/plugin-context/context';
|
||||
|
||||
const GenericComponentPluginStateContainer = ((
|
||||
props: ExtensibleAreaComponentManagerProps,
|
||||
) => {
|
||||
const {
|
||||
uuid,
|
||||
generateItemWithId,
|
||||
extensibleAreaMap,
|
||||
pluginApi,
|
||||
} = props;
|
||||
const [
|
||||
genericComponents,
|
||||
setGenericComponents,
|
||||
] = useState<PluginSdk.GenericComponentInterface[]>([]);
|
||||
|
||||
const {
|
||||
pluginsExtensibleAreasAggregatedState,
|
||||
setPluginsExtensibleAreasAggregatedState,
|
||||
} = useContext(PluginsContext);
|
||||
|
||||
useEffect(() => {
|
||||
// Change this plugin provided toolbar items
|
||||
extensibleAreaMap[uuid].genericComponents = genericComponents;
|
||||
|
||||
// Update context with computed aggregated list of all plugin provided toolbar items
|
||||
const aggregatedGenericComponents = (
|
||||
[] as PluginSdk.GenericComponentInterface[]).concat(
|
||||
...Object.values(extensibleAreaMap)
|
||||
.map((extensibleArea: ExtensibleArea) => extensibleArea.genericComponents),
|
||||
);
|
||||
setPluginsExtensibleAreasAggregatedState(
|
||||
{
|
||||
...pluginsExtensibleAreasAggregatedState,
|
||||
genericComponents: aggregatedGenericComponents,
|
||||
},
|
||||
);
|
||||
}, [genericComponents]);
|
||||
|
||||
pluginApi.setGenericComponents = (items: PluginSdk.GenericComponentInterface[]) => {
|
||||
const itemsWithId = items.map(generateItemWithId) as PluginSdk.GenericComponentInterface[];
|
||||
return setGenericComponents(itemsWithId);
|
||||
};
|
||||
return null;
|
||||
}) as ExtensibleAreaComponentManager;
|
||||
|
||||
export default GenericComponentPluginStateContainer;
|
@ -17,6 +17,7 @@ import {
|
||||
ExtensibleAreaComponentManager, ExtensibleAreaMap,
|
||||
} from './types';
|
||||
import FloatingWindowPluginStateContainer from './components/floating-window/manager';
|
||||
import GenericComponentPluginStateContainer from './components/generic-component/manager';
|
||||
|
||||
const extensibleAreaMap: ExtensibleAreaMap = {};
|
||||
|
||||
@ -33,6 +34,7 @@ const extensibleAreaComponentManagers: ExtensibleAreaComponentManager[] = [
|
||||
UserCameraDropdownPluginStateContainer,
|
||||
UserListItemAdditionalInformationPluginStateContainer,
|
||||
FloatingWindowPluginStateContainer,
|
||||
GenericComponentPluginStateContainer,
|
||||
];
|
||||
|
||||
function generateItemWithId<T extends PluginProvidedUiItemDescriptor>(
|
||||
|
@ -20,6 +20,7 @@ export interface ExtensibleArea {
|
||||
userCameraDropdownItems: PluginSdk.UserCameraDropdownInterface[];
|
||||
userListItemAdditionalInformation: PluginSdk.UserListItemAdditionalInformationInterface[];
|
||||
floatingWindows: PluginSdk.FloatingWindowInterface[]
|
||||
genericComponents: PluginSdk.GenericComponentInterface[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,8 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import PluginChatUiCommandsHandler from './chat/handler';
|
||||
import PluginLayoutUiCommandsHandler from './layout/handler';
|
||||
|
||||
const PluginUiCommandsHandler = () => (
|
||||
<>
|
||||
<PluginLayoutUiCommandsHandler />
|
||||
<PluginChatUiCommandsHandler />
|
||||
</>
|
||||
);
|
||||
|
@ -0,0 +1,60 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Session } from 'meteor/session';
|
||||
import {
|
||||
LayoutCommandsEnum, LayoutComponentListEnum,
|
||||
} from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-commands/layout/enums';
|
||||
import { layoutDispatch } from '../../../layout/context';
|
||||
import { ACTIONS } from '../../../layout/enums';
|
||||
|
||||
const PluginLayoutUiCommandsHandler = () => {
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
|
||||
const handleLayoutSet = ((event: CustomEvent<LayoutComponentListEnum>) => {
|
||||
const layout = event.detail;
|
||||
switch (layout) {
|
||||
case LayoutComponentListEnum.GENERIC_COMPONENT:
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_GENERIC_COMPONENT,
|
||||
value: true,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}) as EventListener;
|
||||
|
||||
const handleLayoutUnset = ((event: CustomEvent<LayoutComponentListEnum>) => {
|
||||
const layout = event.detail;
|
||||
switch (layout) {
|
||||
case LayoutComponentListEnum.GENERIC_COMPONENT:
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_GENERIC_COMPONENT,
|
||||
value: false,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: Session.get('presentationLastState'),
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}) as EventListener;
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener(LayoutCommandsEnum.SET, handleLayoutSet);
|
||||
window.addEventListener(LayoutCommandsEnum.UNSET, handleLayoutUnset);
|
||||
|
||||
return () => {
|
||||
window.addEventListener(LayoutCommandsEnum.SET, handleLayoutSet);
|
||||
window.addEventListener(LayoutCommandsEnum.UNSET, handleLayoutUnset);
|
||||
};
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default PluginLayoutUiCommandsHandler;
|
@ -906,7 +906,7 @@ export default injectIntl(Presentation);
|
||||
|
||||
Presentation.propTypes = {
|
||||
// Defines a boolean value to detect whether a current user is a presenter
|
||||
userIsPresenter: PropTypes.bool.isRequired,
|
||||
userIsPresenter: PropTypes.bool,
|
||||
currentSlide: PropTypes.shape({
|
||||
presentationId: PropTypes.string.isRequired,
|
||||
current: PropTypes.bool.isRequired,
|
||||
@ -929,9 +929,9 @@ Presentation.propTypes = {
|
||||
multiUser: PropTypes.bool.isRequired,
|
||||
setPresentationIsOpen: PropTypes.func.isRequired,
|
||||
layoutContextDispatch: PropTypes.func.isRequired,
|
||||
presentationIsDownloadable: PropTypes.bool.isRequired,
|
||||
presentationName: PropTypes.string.isRequired,
|
||||
currentPresentationId: PropTypes.string.isRequired,
|
||||
presentationIsDownloadable: PropTypes.bool,
|
||||
presentationName: PropTypes.string,
|
||||
currentPresentationId: PropTypes.string,
|
||||
presentationIsOpen: PropTypes.bool.isRequired,
|
||||
totalPages: PropTypes.number.isRequired,
|
||||
publishedPoll: PropTypes.bool.isRequired,
|
||||
@ -970,4 +970,8 @@ Presentation.defaultProps = {
|
||||
presentationAreaSize: undefined,
|
||||
presentationBounds: undefined,
|
||||
downloadPresentationUri: undefined,
|
||||
userIsPresenter: false,
|
||||
presentationIsDownloadable: false,
|
||||
currentPresentationId: '',
|
||||
presentationName: '',
|
||||
};
|
||||
|
@ -12,7 +12,7 @@ import Styled from './styles';
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import TooltipContainer from '/imports/ui/components/common/tooltip/container';
|
||||
import { ACTIONS } from '/imports/ui/components/layout/enums';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import AppService from '/imports/ui/components/app/service';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -280,9 +280,9 @@ const PresentationMenu = (props) => {
|
||||
);
|
||||
}
|
||||
|
||||
const { isSafari } = browserInfo;
|
||||
const { isIos } = deviceInfo;
|
||||
|
||||
if (!isSafari && allowSnapshotOfCurrentSlide) {
|
||||
if (allowSnapshotOfCurrentSlide) {
|
||||
menuItems.push(
|
||||
{
|
||||
key: 'list-item-screenshot',
|
||||
@ -322,18 +322,38 @@ const PresentationMenu = (props) => {
|
||||
&& shape.y >= 0,
|
||||
);
|
||||
const svgElem = await tldrawAPI.getSvg(shapes.map((shape) => shape.id));
|
||||
const width = svgElem?.width?.baseVal?.value ?? window.screen.width;
|
||||
const height = svgElem?.height?.baseVal?.value ?? window.screen.height;
|
||||
|
||||
const data = await toPng(svgElem, { width, height, backgroundColor: '#FFF' });
|
||||
// workaround for ios
|
||||
if (isIos) {
|
||||
svgElem.setAttribute('width', backgroundShape.props.w);
|
||||
svgElem.setAttribute('height', backgroundShape.props.h);
|
||||
svgElem.setAttribute('viewBox', `1 1 ${backgroundShape.props.w} ${backgroundShape.props.h}`);
|
||||
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = data;
|
||||
anchor.setAttribute(
|
||||
'download',
|
||||
`${elementName}_${meetingName}_${new Date().toISOString()}.png`,
|
||||
);
|
||||
anchor.click();
|
||||
const svgString = new XMLSerializer().serializeToString(svgElem);
|
||||
const blob = new Blob([svgString], { type: 'image/svg+xml' });
|
||||
|
||||
const data = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = data;
|
||||
anchor.setAttribute(
|
||||
'download',
|
||||
`${elementName}_${meetingName}_${new Date().toISOString()}.svg`,
|
||||
);
|
||||
anchor.click();
|
||||
} else {
|
||||
const width = svgElem?.width?.baseVal?.value ?? window.screen.width;
|
||||
const height = svgElem?.height?.baseVal?.value ?? window.screen.height;
|
||||
|
||||
const data = await toPng(svgElem, { width, height, backgroundColor: '#FFF' });
|
||||
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = data;
|
||||
anchor.setAttribute(
|
||||
'download',
|
||||
`${elementName}_${meetingName}_${new Date().toISOString()}.png`,
|
||||
);
|
||||
anchor.click();
|
||||
}
|
||||
|
||||
setState({
|
||||
loading: false,
|
||||
|
@ -117,9 +117,6 @@ PresentationToolbarContainer.propTypes = {
|
||||
numberOfSlides: PropTypes.number.isRequired,
|
||||
|
||||
// Actions required for the presenter toolbar
|
||||
nextSlide: PropTypes.func.isRequired,
|
||||
previousSlide: PropTypes.func.isRequired,
|
||||
skipToSlide: PropTypes.func.isRequired,
|
||||
layoutSwapped: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
@ -44,13 +44,13 @@ const propTypes = {
|
||||
allowDownloadConverted: PropTypes.bool.isRequired,
|
||||
allowDownloadWithAnnotations: PropTypes.bool.isRequired,
|
||||
item: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
filename: PropTypes.string.isRequired,
|
||||
id: PropTypes.string,
|
||||
filename: PropTypes.string,
|
||||
filenameConverted: PropTypes.string,
|
||||
isCurrent: PropTypes.bool.isRequired,
|
||||
isCurrent: PropTypes.bool,
|
||||
temporaryPresentationId: PropTypes.string,
|
||||
isDownloadable: PropTypes.bool.isRequired,
|
||||
isRemovable: PropTypes.bool.isRequired,
|
||||
isDownloadable: PropTypes.bool,
|
||||
isRemovable: PropTypes.bool,
|
||||
conversion: PropTypes.shape({
|
||||
done: PropTypes.bool,
|
||||
error: PropTypes.bool,
|
||||
@ -61,17 +61,21 @@ const propTypes = {
|
||||
upload: PropTypes.shape({
|
||||
done: PropTypes.bool,
|
||||
error: PropTypes.bool,
|
||||
}).isRequired,
|
||||
}),
|
||||
exportation: PropTypes.shape({
|
||||
status: PropTypes.string,
|
||||
}),
|
||||
uploadTimestamp: PropTypes.string,
|
||||
downloadableExtension: PropTypes.string,
|
||||
}).isRequired,
|
||||
}),
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
item: {},
|
||||
};
|
||||
|
||||
class PresentationDownloadDropdown extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -204,5 +208,6 @@ class PresentationDownloadDropdown extends PureComponent {
|
||||
}
|
||||
|
||||
PresentationDownloadDropdown.propTypes = propTypes;
|
||||
PresentationDownloadDropdown.defaultProps = defaultProps;
|
||||
|
||||
export default injectIntl(PresentationDownloadDropdown);
|
||||
|
@ -125,7 +125,6 @@ const PresentationContainer = styled.div`
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const Presentation = styled.div`
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { borderSize, navbarHeight } from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { borderSize, navbarHeight, smPaddingX } from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { smallOnly, mediumUp } from '/imports/ui/stylesheets/styled-components/breakpoints';
|
||||
|
||||
const Poll = styled.div`
|
||||
@ -16,7 +16,7 @@ const Poll = styled.div`
|
||||
height: 100%;
|
||||
background-color: ${colorWhite};
|
||||
min-width: 20em;
|
||||
padding: 1rem;
|
||||
padding: ${smPaddingX};
|
||||
|
||||
@media ${smallOnly} {
|
||||
top: 0;
|
||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Service from './service';
|
||||
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
|
||||
import Header from '/imports/ui/components/common/control-header/component';
|
||||
import Styled from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -81,7 +82,6 @@ const propTypes = {
|
||||
}).isRequired,
|
||||
layoutContextDispatch: PropTypes.shape().isRequired,
|
||||
timeOffset: PropTypes.number.isRequired,
|
||||
isRTL: PropTypes.bool.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
isModerator: PropTypes.bool.isRequired,
|
||||
currentTrack: PropTypes.string.isRequired,
|
||||
@ -434,7 +434,6 @@ class Timer extends Component {
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
isRTL,
|
||||
isActive,
|
||||
isModerator,
|
||||
layoutContextDispatch,
|
||||
@ -453,16 +452,13 @@ class Timer extends Component {
|
||||
<Styled.TimerSidebarContent
|
||||
data-test="timer"
|
||||
>
|
||||
<Styled.TimerHeader>
|
||||
<Styled.TimerTitle>
|
||||
<Styled.TimerMinimizeButton
|
||||
onClick={() => Service.closePanel(layoutContextDispatch)}
|
||||
aria-label={intl.formatMessage(intlMessages.hideTimerLabel)}
|
||||
label={intl.formatMessage(message)}
|
||||
icon={isRTL ? 'right_arrow' : 'left_arrow'}
|
||||
/>
|
||||
</Styled.TimerTitle>
|
||||
</Styled.TimerHeader>
|
||||
<Header
|
||||
leftButtonProps={{
|
||||
onClick: () => { Service.closePanel(layoutContextDispatch); },
|
||||
'aria-label': intl.formatMessage(intlMessages.hideTimerLabel),
|
||||
label: intl.formatMessage(message),
|
||||
}}
|
||||
/>
|
||||
{this.renderContent()}
|
||||
</Styled.TimerSidebarContent>
|
||||
);
|
||||
|
@ -115,13 +115,9 @@ const TimerContainer = ({ children, ...props }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default withTracker(() => {
|
||||
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
|
||||
return {
|
||||
isRTL,
|
||||
isActive: Service.isActive(),
|
||||
timeOffset: Service.getTimeOffset(),
|
||||
timer: Service.getTimer(),
|
||||
currentTrack: Service.getCurrentTrack(),
|
||||
};
|
||||
})(TimerContainer);
|
||||
export default withTracker(() => ({
|
||||
isActive: Service.isActive(),
|
||||
timeOffset: Service.getTimeOffset(),
|
||||
timer: Service.getTimer(),
|
||||
currentTrack: Service.getCurrentTrack(),
|
||||
}))(TimerContainer);
|
||||
|
@ -2,9 +2,7 @@ import styled from 'styled-components';
|
||||
import {
|
||||
borderSize,
|
||||
borderSizeLarge,
|
||||
mdPaddingX,
|
||||
mdPaddingY,
|
||||
pollHeaderOffset,
|
||||
smPaddingX,
|
||||
toastContentWidth,
|
||||
borderRadius,
|
||||
} from '../../stylesheets/styled-components/general';
|
||||
@ -22,12 +20,7 @@ import Button from '/imports/ui/components/common/button/component';
|
||||
|
||||
const TimerSidebarContent = styled.div`
|
||||
background-color: ${colorWhite};
|
||||
padding:
|
||||
${mdPaddingX}
|
||||
${mdPaddingY}
|
||||
${mdPaddingX}
|
||||
${mdPaddingX};
|
||||
|
||||
padding: ${smPaddingX};
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
@ -39,7 +32,6 @@ const TimerSidebarContent = styled.div`
|
||||
|
||||
const TimerHeader = styled.header`
|
||||
position: relative;
|
||||
top: ${pollHeaderOffset};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
@ -13,10 +13,22 @@ import { isChatEnabled } from '/imports/ui/services/features';
|
||||
import UserTitleContainer from '../user-list-graphql/user-participants-title/component';
|
||||
|
||||
const propTypes = {
|
||||
currentUser: PropTypes.shape({}).isRequired,
|
||||
currentUser: PropTypes.shape({
|
||||
role: PropTypes.string.isRequired,
|
||||
presenter: PropTypes.bool.isRequired,
|
||||
}),
|
||||
compact: PropTypes.bool,
|
||||
isTimerActive: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
currentUser: {
|
||||
role: '',
|
||||
presenter: false,
|
||||
},
|
||||
compact: false,
|
||||
};
|
||||
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
|
||||
class UserContent extends PureComponent {
|
||||
@ -46,5 +58,6 @@ class UserContent extends PureComponent {
|
||||
}
|
||||
|
||||
UserContent.propTypes = propTypes;
|
||||
UserContent.defaultProps = defaultProps;
|
||||
|
||||
export default UserContent;
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import React, { useState, useContext } from 'react';
|
||||
import { User } from '/imports/ui/Types/user';
|
||||
import { LockSettings, UsersPolicies } from '/imports/ui/Types/meeting';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import { UserListDropdownItemType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/extensible-areas/user-list-dropdown-item/enums';
|
||||
import {
|
||||
SET_AWAY,
|
||||
@ -65,6 +66,11 @@ interface DropdownItem {
|
||||
onClick: (() => void) | undefined;
|
||||
}
|
||||
|
||||
interface Writer {
|
||||
pageId: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
statusTriggerLabel: {
|
||||
id: 'app.actionsBar.emojiMenu.statusTriggerLabel',
|
||||
@ -214,30 +220,39 @@ const UserActions: React.FC<UserActionsProps> = ({
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
|
||||
const [presentationSetWriters] = useMutation(PRESENTATION_SET_WRITERS);
|
||||
const [getWriters, { data: usersData }] = useLazyQuery(CURRENT_PAGE_WRITERS_QUERY, { fetchPolicy: 'no-cache' });
|
||||
const writers = usersData?.pres_page_writers || null;
|
||||
const [getWriters] = useLazyQuery(CURRENT_PAGE_WRITERS_QUERY, { fetchPolicy: 'no-cache' });
|
||||
const voiceToggle = useToggleVoice();
|
||||
|
||||
// users will only be fetched when getWriters is called
|
||||
useEffect(() => {
|
||||
if (writers) {
|
||||
changeWhiteboardAccess();
|
||||
}
|
||||
}, [writers]);
|
||||
const handleWhiteboardAccessChange = async () => {
|
||||
try {
|
||||
// Fetch the writers data
|
||||
const { data } = await getWriters();
|
||||
const allWriters: Writer[] = data?.pres_page_writers || [];
|
||||
const currentWriters = allWriters?.filter((writer: Writer) => writer.pageId === pageId);
|
||||
|
||||
const changeWhiteboardAccess = () => {
|
||||
if (pageId) {
|
||||
const { userId } = user;
|
||||
const usersIds = writers.map((writer: { userId: string }) => writer.userId);
|
||||
const hasAccess = writers?.some((writer: { userId: string }) => writer.userId === userId);
|
||||
const newUsersIds = hasAccess ? usersIds.filter((id: string) => id !== userId) : [...usersIds, userId];
|
||||
// Determine if the user has access
|
||||
const { userId, presPagesWritable } = user;
|
||||
const hasAccess = presPagesWritable.some(
|
||||
(page: { userId: string; isCurrentPage: boolean }) => (page?.userId === userId && page?.isCurrentPage),
|
||||
);
|
||||
|
||||
presentationSetWriters({
|
||||
// Prepare the updated list of user IDs for whiteboard access
|
||||
const usersIds = currentWriters?.map((writer: { userId: string }) => writer?.userId);
|
||||
const newUsersIds: string[] = hasAccess
|
||||
? usersIds.filter((id: string) => id !== userId)
|
||||
: [...usersIds, userId];
|
||||
|
||||
// Update the writers
|
||||
await presentationSetWriters({
|
||||
variables: {
|
||||
pageId,
|
||||
usersIds: newUsersIds,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn({
|
||||
logCode: 'user_action_whiteboard_access_failed',
|
||||
}, 'Error updating whiteboard access.');
|
||||
}
|
||||
};
|
||||
|
||||
@ -285,7 +300,9 @@ const UserActions: React.FC<UserActionsProps> = ({
|
||||
(item: PluginSdk.UserListDropdownInterface) => (user?.userId === item?.userId),
|
||||
);
|
||||
|
||||
const hasWhiteboardAccess = user.presPagesWritable?.length > 0;
|
||||
const hasWhiteboardAccess = user.presPagesWritable?.some(
|
||||
(page: { pageId: string; userId: string }) => (page.pageId === pageId && page.userId === user.userId),
|
||||
);
|
||||
|
||||
const [setAway] = useMutation(SET_AWAY);
|
||||
const [setRole] = useMutation(SET_ROLE);
|
||||
@ -434,7 +451,7 @@ const UserActions: React.FC<UserActionsProps> = ({
|
||||
? intl.formatMessage(messages.removeWhiteboardAccess)
|
||||
: intl.formatMessage(messages.giveWhiteboardAccess),
|
||||
onClick: () => {
|
||||
getWriters();
|
||||
handleWhiteboardAccessChange();
|
||||
setSelected(false);
|
||||
},
|
||||
icon: 'pen_tool',
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Service from './service';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import VideoPreview from './component';
|
||||
import VideoService from '../video-provider/service';
|
||||
import ScreenShareService from '/imports/ui/components/screenshare/service';
|
||||
@ -10,9 +10,8 @@ import { SCREENSHARING_ERRORS } from '/imports/api/screenshare/client/bridge/err
|
||||
import { EXTERNAL_VIDEO_STOP } from '../external-video-player/mutations';
|
||||
import { CAMERA_BROADCAST_STOP } from '../video-provider/mutations';
|
||||
|
||||
const VideoPreviewContainer = (props) => <VideoPreview {...props} />;
|
||||
|
||||
export default withTracker(({ setIsOpen, callbackToClose }) => {
|
||||
const VideoPreviewContainer = (props) => {
|
||||
const { buildStartSharingCameraAsContent, buildStopSharing, ...rest } = props;
|
||||
const [stopExternalVideoShare] = useMutation(EXTERNAL_VIDEO_STOP);
|
||||
const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP);
|
||||
|
||||
@ -20,58 +19,71 @@ export default withTracker(({ setIsOpen, callbackToClose }) => {
|
||||
cameraBroadcastStop({ variables: { cameraId } });
|
||||
};
|
||||
|
||||
return {
|
||||
startSharing: (deviceId) => {
|
||||
callbackToClose();
|
||||
setIsOpen(false);
|
||||
VideoService.joinVideo(deviceId);
|
||||
},
|
||||
startSharingCameraAsContent: (deviceId) => {
|
||||
callbackToClose();
|
||||
setIsOpen(false);
|
||||
const handleFailure = (error) => {
|
||||
const {
|
||||
errorCode = SCREENSHARING_ERRORS.UNKNOWN_ERROR.errorCode,
|
||||
errorMessage = error.message,
|
||||
} = error;
|
||||
const startSharingCameraAsContent = buildStartSharingCameraAsContent(stopExternalVideoShare);
|
||||
const stopSharing = buildStopSharing(sendUserUnshareWebcam);
|
||||
|
||||
logger.error({
|
||||
logCode: 'camera_as_content_failed',
|
||||
extraInfo: { errorCode, errorMessage },
|
||||
}, `Sharing camera as content failed: ${errorMessage} (code=${errorCode})`);
|
||||
return (
|
||||
<VideoPreview
|
||||
{...{
|
||||
startSharingCameraAsContent,
|
||||
stopSharing,
|
||||
...rest,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTracker(({ setIsOpen, callbackToClose }) => ({
|
||||
startSharing: (deviceId) => {
|
||||
callbackToClose();
|
||||
setIsOpen(false);
|
||||
VideoService.joinVideo(deviceId);
|
||||
},
|
||||
buildStartSharingCameraAsContent: (stopExternalVideoShare) => (deviceId) => {
|
||||
callbackToClose();
|
||||
setIsOpen(false);
|
||||
const handleFailure = (error) => {
|
||||
const {
|
||||
errorCode = SCREENSHARING_ERRORS.UNKNOWN_ERROR.errorCode,
|
||||
errorMessage = error.message,
|
||||
} = error;
|
||||
|
||||
logger.error({
|
||||
logCode: 'camera_as_content_failed',
|
||||
extraInfo: { errorCode, errorMessage },
|
||||
}, `Sharing camera as content failed: ${errorMessage} (code=${errorCode})`);
|
||||
|
||||
ScreenShareService.screenshareHasEnded();
|
||||
};
|
||||
ScreenShareService.shareScreen(
|
||||
stopExternalVideoShare,
|
||||
true, handleFailure, { stream: Service.getStream(deviceId)._mediaStream }
|
||||
);
|
||||
ScreenShareService.setCameraAsContentDeviceId(deviceId);
|
||||
},
|
||||
stopSharing: (deviceId) => {
|
||||
callbackToClose();
|
||||
setIsOpen(false);
|
||||
if (deviceId) {
|
||||
const streamId = VideoService.getMyStreamId(deviceId);
|
||||
if (streamId) VideoService.stopVideo(streamId, sendUserUnshareWebcam);
|
||||
} else {
|
||||
VideoService.exitVideo(sendUserUnshareWebcam);
|
||||
}
|
||||
},
|
||||
stopSharingCameraAsContent: () => {
|
||||
callbackToClose();
|
||||
setIsOpen(false);
|
||||
ScreenShareService.screenshareHasEnded();
|
||||
},
|
||||
sharedDevices: VideoService.getSharedDevices(),
|
||||
cameraAsContentDeviceId: ScreenShareService.getCameraAsContentDeviceId(),
|
||||
isCamLocked: VideoService.isUserLocked(),
|
||||
camCapReached: VideoService.hasCapReached(),
|
||||
closeModal: () => {
|
||||
callbackToClose();
|
||||
setIsOpen(false);
|
||||
},
|
||||
webcamDeviceId: Service.webcamDeviceId(),
|
||||
hasVideoStream: VideoService.hasVideoStream(),
|
||||
};
|
||||
})(VideoPreviewContainer);
|
||||
};
|
||||
ScreenShareService.shareScreen(
|
||||
stopExternalVideoShare,
|
||||
true, handleFailure, { stream: Service.getStream(deviceId)._mediaStream }
|
||||
);
|
||||
ScreenShareService.setCameraAsContentDeviceId(deviceId);
|
||||
},
|
||||
buildStopSharing: (sendUserUnshareWebcam) => (deviceId) => {
|
||||
callbackToClose();
|
||||
setIsOpen(false);
|
||||
if (deviceId) {
|
||||
const streamId = VideoService.getMyStreamId(deviceId);
|
||||
if (streamId) VideoService.stopVideo(streamId, sendUserUnshareWebcam);
|
||||
} else {
|
||||
VideoService.exitVideo(sendUserUnshareWebcam);
|
||||
}
|
||||
},
|
||||
stopSharingCameraAsContent: () => {
|
||||
callbackToClose();
|
||||
setIsOpen(false);
|
||||
ScreenShareService.screenshareHasEnded();
|
||||
},
|
||||
sharedDevices: VideoService.getSharedDevices(),
|
||||
cameraAsContentDeviceId: ScreenShareService.getCameraAsContentDeviceId(),
|
||||
isCamLocked: VideoService.isUserLocked(),
|
||||
camCapReached: VideoService.hasCapReached(),
|
||||
closeModal: () => {
|
||||
callbackToClose();
|
||||
setIsOpen(false);
|
||||
},
|
||||
webcamDeviceId: Service.webcamDeviceId(),
|
||||
hasVideoStream: VideoService.hasVideoStream(),
|
||||
}))(VideoPreviewContainer);
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import {
|
||||
borderSize,
|
||||
mdPaddingX,
|
||||
smPaddingX,
|
||||
mdPaddingY,
|
||||
userIndicatorsOffset,
|
||||
indicatorPadding,
|
||||
@ -198,8 +198,7 @@ const CustomButton = styled(Button)`
|
||||
|
||||
const Panel = styled.div<PanelProps>`
|
||||
background-color: ${colorWhite};
|
||||
padding: ${mdPaddingX} ${mdPaddingY} ${mdPaddingX} ${mdPaddingX};
|
||||
|
||||
padding: ${smPaddingX};
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
|
@ -38,18 +38,6 @@ const TOOLBAR_SMALL = 28;
|
||||
const TOOLBAR_LARGE = 32;
|
||||
const MOUNTED_RESIZE_DELAY = 1500;
|
||||
|
||||
// Shallow cloning with nested structures
|
||||
const deepCloneUsingShallow = (obj) => {
|
||||
const clonedObj = clone(obj);
|
||||
if (obj.props) {
|
||||
clonedObj.props = clone(obj.props);
|
||||
}
|
||||
if (obj.props) {
|
||||
clonedObj.meta = clone(obj.meta);
|
||||
}
|
||||
return clonedObj;
|
||||
};
|
||||
|
||||
// Helper functions
|
||||
const deleteLocalStorageItemsWithPrefix = (prefix) => {
|
||||
const keysToRemove = Object.keys(localStorage).filter((key) =>
|
||||
@ -166,18 +154,37 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
const isMouseDownRef = useRef(false);
|
||||
const isMountedRef = useRef(false);
|
||||
const isWheelZoomRef = useRef(false);
|
||||
const whiteboardIdRef = React.useRef(whiteboardId);
|
||||
const curPageIdRef = React.useRef(curPageId);
|
||||
const hasWBAccessRef = React.useRef(hasWBAccess);
|
||||
|
||||
const THRESHOLD = 0.1;
|
||||
const lastKnownHeight = React.useRef(presentationAreaHeight);
|
||||
const lastKnownWidth = React.useRef(presentationAreaWidth);
|
||||
|
||||
React.useEffect(() => {
|
||||
curPageIdRef.current = curPageId;
|
||||
}, [curPageId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
whiteboardIdRef.current = whiteboardId;
|
||||
}, [whiteboardId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
hasWBAccessRef.current = hasWBAccess;
|
||||
|
||||
if (!hasWBAccess && !isPresenter) {
|
||||
tlEditorRef?.current?.setCurrentTool('select');
|
||||
}
|
||||
}, [hasWBAccess]);
|
||||
|
||||
const language = React.useMemo(() => {
|
||||
return mapLanguage(Settings?.application?.locale?.toLowerCase() || "en");
|
||||
}, [Settings?.application?.locale]);
|
||||
|
||||
const [cursorPosition, updateCursorPosition] = useCursor(
|
||||
publishCursorUpdate,
|
||||
whiteboardId
|
||||
whiteboardIdRef.current
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
@ -195,20 +202,16 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
const { shapesToAdd, shapesToUpdate, shapesToRemove } = React.useMemo(() => {
|
||||
const selectedShapeIds = tlEditorRef.current?.selectedShapeIds || [];
|
||||
const localShapes = tlEditorRef.current?.currentPageShapes;
|
||||
const filteredShapes =
|
||||
localShapes?.filter((item) => item?.index !== "a0") || [];
|
||||
const localLookup = new Map(
|
||||
filteredShapes.map((shape) => [shape.id, shape])
|
||||
);
|
||||
const filteredShapes = localShapes?.filter((item) => item?.index !== "a0") || [];
|
||||
const localLookup = new Map(filteredShapes.map((shape) => [shape.id, shape]));
|
||||
const remoteShapeIds = Object.keys(prevShapesRef.current);
|
||||
const toAdd = [];
|
||||
const toUpdate = [];
|
||||
const toRemove = [];
|
||||
|
||||
filteredShapes.forEach((localShape) => {
|
||||
// If a local shape does not exist in the remote shapes, it should be removed
|
||||
if (!remoteShapeIds.includes(localShape.id)) {
|
||||
toRemove.push(localShape);
|
||||
toRemove.push(localShape.id);
|
||||
}
|
||||
});
|
||||
|
||||
@ -216,18 +219,14 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
if (!remoteShape.id) return;
|
||||
const localShape = localLookup.get(remoteShape.id);
|
||||
const prevShape = prevShapesRef.current[remoteShape.id];
|
||||
// Create a deep clone of remoteShape and remove the isModerator property
|
||||
const comparisonRemoteShape = deepCloneUsingShallow(remoteShape);
|
||||
delete comparisonRemoteShape.isModerator;
|
||||
delete comparisonRemoteShape.questionType;
|
||||
|
||||
if (!localShape) {
|
||||
if (prevShapesRef.current[`${remoteShape.id}`].meta?.createdBy !== currentUser?.userId) {
|
||||
// If the shape does not exist in local, add it to toAdd
|
||||
delete remoteShape.isModerator
|
||||
delete remoteShape.questionType
|
||||
toAdd.push(remoteShape);
|
||||
}
|
||||
} else if (!isEqual(localShape, comparisonRemoteShape) && prevShape) {
|
||||
// Capture the differences
|
||||
} else if (!isEqual(localShape, remoteShape) && prevShape) {
|
||||
const diff = {
|
||||
id: remoteShape.id,
|
||||
type: remoteShape.type,
|
||||
@ -235,12 +234,8 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
};
|
||||
|
||||
if (!selectedShapeIds.includes(remoteShape.id) && prevShape?.meta?.updatedBy !== currentUser?.userId) {
|
||||
// Compare each property
|
||||
Object.keys(remoteShape).forEach((key) => {
|
||||
if (
|
||||
key !== "isModerator" &&
|
||||
!isEqual(remoteShape[key], localShape[key])
|
||||
) {
|
||||
if (key !== "isModerator" && !isEqual(remoteShape[key], localShape[key])) {
|
||||
diff[key] = remoteShape[key];
|
||||
}
|
||||
});
|
||||
@ -254,26 +249,19 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
});
|
||||
}
|
||||
|
||||
delete diff.isModerator
|
||||
delete diff.questionType
|
||||
toUpdate.push(diff);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
toAdd.forEach((shape) => {
|
||||
delete shape.isModerator;
|
||||
delete shape.questionType;
|
||||
});
|
||||
toUpdate.forEach((shape) => {
|
||||
delete shape.isModerator;
|
||||
delete shape.questionType;
|
||||
});
|
||||
|
||||
return {
|
||||
shapesToAdd: toAdd,
|
||||
shapesToUpdate: toUpdate,
|
||||
shapesToRemove: toRemove,
|
||||
};
|
||||
}, [prevShapesRef.current]);
|
||||
}, [prevShapesRef.current, curPageIdRef.current]);
|
||||
|
||||
const setCamera = (zoom, x = 0, y = 0) => {
|
||||
if (tlEditorRef.current) {
|
||||
@ -310,12 +298,12 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
{ whiteboardRef, tlEditorRef, isWheelZoomRef, initialZoomRef },
|
||||
{
|
||||
isPresenter,
|
||||
hasWBAccess,
|
||||
hasWBAccess: hasWBAccessRef.current,
|
||||
isMouseDownRef,
|
||||
whiteboardToolbarAutoHide,
|
||||
animations,
|
||||
publishCursorUpdate,
|
||||
whiteboardId,
|
||||
whiteboardId: whiteboardIdRef.current,
|
||||
cursorPosition,
|
||||
updateCursorPosition,
|
||||
toggleToolsAnimations,
|
||||
@ -329,10 +317,58 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
tlEditorRef.current = tlEditor;
|
||||
}, [tlEditor]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let undoRedoIntervalId = null;
|
||||
|
||||
const undo = () => {
|
||||
tlEditorRef?.current?.history?.undo();
|
||||
};
|
||||
|
||||
const redo = () => {
|
||||
tlEditorRef?.current?.history?.redo();
|
||||
};
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if ((event.ctrlKey && event.key === 'z' && !event.shiftKey) || (event.ctrlKey && event.shiftKey && event.key === 'Z')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (!undoRedoIntervalId) {
|
||||
undoRedoIntervalId = setInterval(() => {
|
||||
if (event.ctrlKey && event.key === 'z' && !event.shiftKey) {
|
||||
undo();
|
||||
} else if (event.ctrlKey && event.shiftKey && event.key === 'Z') {
|
||||
redo();
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (event) => {
|
||||
if ((event.key === 'z' || event.key === 'Z') && undoRedoIntervalId) {
|
||||
clearInterval(undoRedoIntervalId);
|
||||
undoRedoIntervalId = null;
|
||||
}
|
||||
};
|
||||
|
||||
whiteboardRef.current?.addEventListener('keydown', handleKeyDown, { capture: true });
|
||||
whiteboardRef.current?.addEventListener('keyup', handleKeyUp, { capture: true });
|
||||
|
||||
return () => {
|
||||
whiteboardRef.current?.removeEventListener('keydown', handleKeyDown);
|
||||
whiteboardRef.current?.removeEventListener('keyup', handleKeyUp);
|
||||
if (undoRedoIntervalId) {
|
||||
clearInterval(undoRedoIntervalId);
|
||||
}
|
||||
};
|
||||
}, [whiteboardRef.current]);
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
zoomValueRef.current = zoomValue;
|
||||
|
||||
if (tlEditor && curPageId && currentPresentationPage && isPresenter && isWheelZoomRef.current === false) {
|
||||
if (tlEditor && curPageIdRef.current && currentPresentationPage && isPresenter && isWheelZoomRef.current === false) {
|
||||
const zoomFitSlide = calculateZoomValue(
|
||||
currentPresentationPage.scaledWidth,
|
||||
currentPresentationPage.scaledHeight
|
||||
@ -391,7 +427,7 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
|
||||
// Update the previous zoom value ref with the current zoom value
|
||||
prevZoomValueRef.current = zoomValue;
|
||||
}, [zoomValue, tlEditor, curPageId, isWheelZoomRef.current]);
|
||||
}, [zoomValue, tlEditor, curPageIdRef.current, isWheelZoomRef.current]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
@ -496,7 +532,7 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [presentationAreaHeight, presentationAreaWidth, curPageId]);
|
||||
}, [presentationAreaHeight, presentationAreaWidth, curPageIdRef.current]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!fitToWidth && isPresenter) {
|
||||
@ -549,17 +585,21 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
React.useEffect(() => {
|
||||
// Check if there are any changes to be made
|
||||
if (shapesToAdd.length || shapesToUpdate.length || shapesToRemove.length) {
|
||||
tlEditor?.store?.mergeRemoteChanges(() => {
|
||||
if (shapesToRemove.length > 0) {
|
||||
tlEditor?.store?.remove(shapesToRemove.map((shape) => shape.id));
|
||||
}
|
||||
if (shapesToAdd.length) {
|
||||
tlEditor?.store?.put(shapesToAdd);
|
||||
}
|
||||
if (shapesToUpdate.length) {
|
||||
tlEditor?.updateShapes(shapesToUpdate);
|
||||
}
|
||||
});
|
||||
const tlStoreUpdateTimeoutId = setTimeout(() => {
|
||||
tlEditor?.store?.mergeRemoteChanges(() => {
|
||||
if (shapesToRemove.length > 0) {
|
||||
tlEditor?.store?.remove(shapesToRemove);
|
||||
}
|
||||
if (shapesToAdd.length) {
|
||||
tlEditor?.store?.put(shapesToAdd);
|
||||
}
|
||||
if (shapesToUpdate.length) {
|
||||
tlEditor?.updateShapes(shapesToUpdate);
|
||||
}
|
||||
});
|
||||
}, 150);
|
||||
|
||||
return () => clearTimeout(tlStoreUpdateTimeoutId);
|
||||
}
|
||||
}, [shapesToAdd, shapesToUpdate, shapesToRemove]);
|
||||
|
||||
@ -617,30 +657,39 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
|
||||
// set current tldraw page when presentation id updates
|
||||
React.useEffect(() => {
|
||||
if (tlEditor && curPageId !== "0") {
|
||||
// Check if the page exists
|
||||
const pageExists =
|
||||
tlEditorRef.current.currentPageId === `page:${curPageId}`;
|
||||
if (tlEditorRef.current && curPageIdRef.current !== "0") {
|
||||
const pages = [
|
||||
{
|
||||
meta: {},
|
||||
id: `page:${curPageIdRef.current}`,
|
||||
name: `Slide ${curPageIdRef.current}`,
|
||||
index: `a1`,
|
||||
typeName: "page",
|
||||
},
|
||||
];
|
||||
|
||||
// If the page does not exist, create it
|
||||
if (!pageExists) {
|
||||
tlEditorRef.current.createPage({ id: `page:${curPageId}` });
|
||||
}
|
||||
|
||||
// Set the current page
|
||||
tlEditor.setCurrentPage(`page:${curPageId}`);
|
||||
tlEditorRef.current.store.mergeRemoteChanges(() => {
|
||||
tlEditorRef.current.batch(() => {
|
||||
tlEditorRef.current.store.put(pages);
|
||||
tlEditorRef.current.deletePage(tlEditorRef.current.currentPageId);
|
||||
tlEditorRef.current.setCurrentPage(`page:${curPageIdRef.current}`);
|
||||
tlEditorRef.current.store.put(assets);
|
||||
tlEditorRef.current.createShapes(bgShape);
|
||||
tlEditorRef.current.history.clear();
|
||||
});
|
||||
});
|
||||
|
||||
whiteboardToolbarAutoHide &&
|
||||
toggleToolsAnimations(
|
||||
"fade-in",
|
||||
"fade-out",
|
||||
"0s",
|
||||
hasWBAccess || isPresenter
|
||||
hasWBAccessRef.current || isPresenter
|
||||
);
|
||||
slideChanged.current = false;
|
||||
slideNext.current = null;
|
||||
}
|
||||
}, [curPageId]);
|
||||
}, [curPageIdRef.current]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTldrawIsMounting(true);
|
||||
@ -690,7 +739,7 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
}, [
|
||||
isMountedRef.current,
|
||||
presentationId,
|
||||
curPageId,
|
||||
curPageIdRef.current,
|
||||
isMultiUserActive,
|
||||
isPresenter,
|
||||
animations,
|
||||
@ -714,8 +763,6 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
|
||||
editor?.user?.updateUserPreferences({ locale: language });
|
||||
|
||||
console.log("EDITOR : ", editor);
|
||||
|
||||
const debouncePersistShape = debounce({ delay: 0 }, persistShapeWrapper);
|
||||
|
||||
const colorStyles = ['black', 'blue', 'green', 'grey', 'light-blue', 'light-green', 'light-red', 'light-violet', 'orange', 'red', 'violet', 'yellow'];
|
||||
@ -753,7 +800,7 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
createdBy: currentUser?.userId,
|
||||
},
|
||||
};
|
||||
persistShapeWrapper(updatedRecord, whiteboardId, isModerator);
|
||||
persistShapeWrapper(updatedRecord, whiteboardIdRef.current, isModerator);
|
||||
});
|
||||
|
||||
Object.values(updated).forEach(([_, record]) => {
|
||||
@ -764,7 +811,7 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
createdBy: shapes[record?.id]?.meta?.createdBy,
|
||||
},
|
||||
};
|
||||
persistShapeWrapper(updatedRecord, whiteboardId, isModerator);
|
||||
persistShapeWrapper(updatedRecord, whiteboardIdRef.current, isModerator);
|
||||
});
|
||||
|
||||
Object.values(removed).forEach((record) => {
|
||||
@ -784,7 +831,7 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
updateCursorPosition(nextPointer?.x, nextPointer?.y);
|
||||
}
|
||||
|
||||
const camKey = `camera:page:${curPageId}`;
|
||||
const camKey = `camera:page:${curPageIdRef.current}`;
|
||||
const { [camKey]: cameras } = updated;
|
||||
if (cameras) {
|
||||
const [prevCam, nextCam] = cameras;
|
||||
@ -813,12 +860,12 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
{ source: "user" }
|
||||
);
|
||||
|
||||
if (editor && curPageId) {
|
||||
if (editor && curPageIdRef.current) {
|
||||
const pages = [
|
||||
{
|
||||
meta: {},
|
||||
id: `page:${curPageId}`,
|
||||
name: `Slide ${curPageId}`,
|
||||
id: `page:${curPageIdRef.current}`,
|
||||
name: `Slide ${curPageIdRef.current}`,
|
||||
index: `a1`,
|
||||
typeName: "page",
|
||||
},
|
||||
@ -828,7 +875,7 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
editor.batch(() => {
|
||||
editor.store.put(pages);
|
||||
editor.deletePage(editor.currentPageId);
|
||||
editor.setCurrentPage(`page:${curPageId}`);
|
||||
editor.setCurrentPage(`page:${curPageIdRef.current}`);
|
||||
editor.store.put(assets);
|
||||
editor.createShapes(bgShape);
|
||||
editor.history.clear();
|
||||
@ -918,33 +965,33 @@ export default Whiteboard = React.memo(function Whiteboard(props) {
|
||||
<div
|
||||
ref={whiteboardRef}
|
||||
id={"whiteboard-element"}
|
||||
key={`animations=-${animations}-${hasWBAccess}-${isPresenter}-${isModerator}-${whiteboardToolbarAutoHide}-${language}`}
|
||||
key={`animations=-${animations}-${isPresenter}-${isModerator}-${whiteboardToolbarAutoHide}-${language}`}
|
||||
>
|
||||
<Tldraw
|
||||
key={`tldrawv2-${curPageId}-${presentationId}-${animations}-${shapes}`}
|
||||
key={`tldrawv2-${presentationId}-${animations}`}
|
||||
forceMobile={true}
|
||||
hideUi={hasWBAccess || isPresenter ? false : true}
|
||||
hideUi={hasWBAccessRef.current || isPresenter ? false : true}
|
||||
onMount={handleTldrawMount}
|
||||
/>
|
||||
<Styled.TldrawV2GlobalStyle
|
||||
{...{ hasWBAccess, isPresenter, isRTL, isMultiUserActive, isToolbarVisible }}
|
||||
{...{ hasWBAccess: hasWBAccessRef.current, isPresenter, isRTL, isMultiUserActive, isToolbarVisible }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Whiteboard.propTypes = {
|
||||
isPresenter: PropTypes.bool.isRequired,
|
||||
isPresenter: PropTypes.bool,
|
||||
isIphone: PropTypes.bool.isRequired,
|
||||
removeShapes: PropTypes.func.isRequired,
|
||||
initDefaultPages: PropTypes.func.isRequired,
|
||||
persistShapeWrapper: PropTypes.func.isRequired,
|
||||
notifyNotAllowedChange: PropTypes.func.isRequired,
|
||||
shapes: PropTypes.objectOf(PropTypes.shape).isRequired,
|
||||
assets: PropTypes.objectOf(PropTypes.shape).isRequired,
|
||||
assets: PropTypes.arrayOf(PropTypes.shape).isRequired,
|
||||
currentUser: PropTypes.shape({
|
||||
userId: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
}),
|
||||
whiteboardId: PropTypes.string,
|
||||
zoomSlide: PropTypes.func.isRequired,
|
||||
curPageId: PropTypes.string.isRequired,
|
||||
@ -980,7 +1027,7 @@ Whiteboard.propTypes = {
|
||||
fullscreenAction: PropTypes.string.isRequired,
|
||||
fullscreenRef: PropTypes.instanceOf(Element),
|
||||
handleToggleFullScreen: PropTypes.func.isRequired,
|
||||
numberOfSlides: PropTypes.number.isRequired,
|
||||
numberOfPages: PropTypes.number,
|
||||
sidebarNavigationWidth: PropTypes.number,
|
||||
presentationId: PropTypes.string,
|
||||
};
|
||||
@ -991,4 +1038,9 @@ Whiteboard.defaultProps = {
|
||||
whiteboardId: undefined,
|
||||
sidebarNavigationWidth: 0,
|
||||
presentationId: undefined,
|
||||
currentUser: {
|
||||
userId: '',
|
||||
},
|
||||
isPresenter: false,
|
||||
numberOfPages: 0,
|
||||
};
|
||||
|
@ -161,7 +161,6 @@ const WhiteboardContainer = (props) => {
|
||||
pageAnnotations,
|
||||
intl,
|
||||
curPageId,
|
||||
pollResults,
|
||||
currentPresentationPage,
|
||||
);
|
||||
|
||||
@ -222,9 +221,8 @@ const WhiteboardContainer = (props) => {
|
||||
const hasShapeAccess = (id) => {
|
||||
const owner = shapes[id]?.meta?.createdBy;
|
||||
const isBackgroundShape = id?.includes(':BG-');
|
||||
const isPollsResult = shapes[id]?.id?.includes('poll-result');
|
||||
const hasAccess = (!isBackgroundShape && !isPollsResult)
|
||||
&& ((owner && owner === currentUser?.userId) || (isPresenter) || (isModerator)) || !shapes[id];
|
||||
const hasAccess = (!isBackgroundShape
|
||||
&& ((owner && owner === currentUser?.userId) || isPresenter || isModerator)) || !shapes[id];
|
||||
|
||||
return hasAccess;
|
||||
};
|
||||
@ -258,7 +256,6 @@ const WhiteboardContainer = (props) => {
|
||||
assets,
|
||||
removeShapes,
|
||||
zoomSlide,
|
||||
numberOfSlides: currentPresentationPage?.totalPages,
|
||||
notifyNotAllowedChange,
|
||||
notifyShapeNumberExceeded,
|
||||
whiteboardToolbarAutoHide:
|
||||
|
@ -165,38 +165,9 @@ const toggleToolsAnimations = (activeAnim, anim, time, hasWBAccess = false) => {
|
||||
checkElementsAndRun();
|
||||
};
|
||||
|
||||
const formatAnnotations = (annotations, intl, curPageId, pollResults, currentPresentationPage) => {
|
||||
const formatAnnotations = (annotations, intl, curPageId, currentPresentationPage) => {
|
||||
const result = {};
|
||||
|
||||
if (pollResults) {
|
||||
// check if pollResults is already added to annotations
|
||||
const hasPollResultsAnnotation = annotations.find(
|
||||
(annotation) => annotation.annotationId === pollResults.pollId,
|
||||
);
|
||||
|
||||
if (!hasPollResultsAnnotation) {
|
||||
const answers = pollResults.responses.map((response) => ({
|
||||
id: response.optionId,
|
||||
key: response.optionDesc,
|
||||
numVotes: response.optionResponsesCount,
|
||||
}));
|
||||
|
||||
const pollResultsAnnotation = {
|
||||
id: pollResults.pollId,
|
||||
annotationInfo: JSON.stringify({
|
||||
answers,
|
||||
id: pollResults.pollId,
|
||||
whiteboardId: curPageId,
|
||||
questionType: true,
|
||||
questionText: pollResults.questionText,
|
||||
}),
|
||||
wbId: curPageId,
|
||||
userId: Auth.userID,
|
||||
};
|
||||
annotations.push(pollResultsAnnotation);
|
||||
}
|
||||
}
|
||||
|
||||
annotations.forEach((annotation) => {
|
||||
if (annotation.annotationInfo === '') return;
|
||||
|
||||
@ -204,70 +175,101 @@ const formatAnnotations = (annotations, intl, curPageId, pollResults, currentPre
|
||||
|
||||
if (annotationInfo.questionType) {
|
||||
// poll result, convert it to text and create tldraw shape
|
||||
annotationInfo.answers = annotationInfo.answers.reduce(
|
||||
caseInsensitiveReducer, [],
|
||||
);
|
||||
let pollResult = PollService.getPollResultString(annotationInfo, intl)
|
||||
.split('<br/>').join('\n').replace(/(<([^>]+)>)/ig, '');
|
||||
if (!annotationInfo.props) {
|
||||
annotationInfo.answers = annotationInfo.answers.reduce(
|
||||
caseInsensitiveReducer, [],
|
||||
);
|
||||
let pollResult = PollService.getPollResultString(annotationInfo, intl)
|
||||
.split('<br/>').join('\n').replace(/(<([^>]+)>)/ig, '');
|
||||
|
||||
const lines = pollResult.split('\n');
|
||||
const longestLine = lines.reduce((a, b) => a.length > b.length ? a : b, '').length;
|
||||
const lines = pollResult.split('\n');
|
||||
const longestLine = lines.reduce((a, b) => (a.length > b.length ? a : b), '').length;
|
||||
|
||||
// add empty spaces before first | in each of the lines to make them all the same length
|
||||
pollResult = lines.map((line) => {
|
||||
if (!line.includes('|') || line.length === longestLine) return line;
|
||||
// add empty spaces before first | in each of the lines to make them all the same length
|
||||
pollResult = lines.map((line) => {
|
||||
if (!line.includes('|') || line.length === longestLine) return line;
|
||||
|
||||
const splitLine = line.split(' |');
|
||||
const spaces = ' '.repeat(longestLine - line.length);
|
||||
return `${splitLine[0]} ${spaces}|${splitLine[1]}`;
|
||||
}).join('\n');
|
||||
const splitLine = line.split(' |');
|
||||
const spaces = ' '.repeat(longestLine - line.length);
|
||||
return `${splitLine[0]} ${spaces}|${splitLine[1]}`;
|
||||
}).join('\n');
|
||||
|
||||
// Text measurement estimation
|
||||
const averageCharWidth = 16;
|
||||
const lineHeight = 32;
|
||||
// Text measurement estimation
|
||||
const averageCharWidth = 16;
|
||||
const lineHeight = 32;
|
||||
|
||||
const annotationWidth = longestLine * averageCharWidth; // Estimate width
|
||||
const annotationHeight = lines.length * lineHeight; // Estimate height
|
||||
const annotationWidth = longestLine * averageCharWidth; // Estimate width
|
||||
const annotationHeight = lines.length * lineHeight; // Estimate height
|
||||
|
||||
const slideWidth = currentPresentationPage?.scaledWidth;
|
||||
const slideHeight = currentPresentationPage?.scaledHeight;
|
||||
const xPosition = slideWidth - annotationWidth;
|
||||
const yPosition = slideHeight - annotationHeight;
|
||||
const slideWidth = currentPresentationPage?.scaledWidth;
|
||||
const slideHeight = currentPresentationPage?.scaledHeight;
|
||||
const xPosition = slideWidth - annotationWidth;
|
||||
const yPosition = slideHeight - annotationHeight;
|
||||
|
||||
let cpg = parseInt(annotationInfo?.id?.split('/')[1]);
|
||||
if (cpg !== parseInt(curPageId)) return;
|
||||
|
||||
annotationInfo = {
|
||||
"x": xPosition,
|
||||
"isLocked": false,
|
||||
"y": yPosition,
|
||||
"rotation": 0,
|
||||
"typeName": "shape",
|
||||
"opacity": 1,
|
||||
"parentId": `page:${curPageId}`,
|
||||
"index": "a1",
|
||||
"id": `shape:poll-result-${annotationInfo.id}`,
|
||||
"meta": {
|
||||
},
|
||||
"type": "geo",
|
||||
"props": {
|
||||
"url": "",
|
||||
"text": `${pollResult}`,
|
||||
"color": "black",
|
||||
"font": "mono",
|
||||
"fill": "semi",
|
||||
"dash": "draw",
|
||||
"h": annotationHeight,
|
||||
"w": annotationWidth,
|
||||
"size": "m",
|
||||
"growY": 0,
|
||||
"align": "middle",
|
||||
"geo": "rectangle",
|
||||
"verticalAlign": "middle",
|
||||
"labelColor": "black"
|
||||
}
|
||||
annotationInfo = {
|
||||
x: xPosition,
|
||||
y: yPosition,
|
||||
isLocked: false,
|
||||
rotation: 0,
|
||||
typeName: 'shape',
|
||||
opacity: 1,
|
||||
parentId: `page:${curPageId}`,
|
||||
index: 'a1',
|
||||
id: `${annotationInfo.id}`,
|
||||
meta: {},
|
||||
type: 'geo',
|
||||
props: {
|
||||
url: '',
|
||||
text: `${pollResult}`,
|
||||
color: 'black',
|
||||
font: 'mono',
|
||||
fill: 'semi',
|
||||
dash: 'draw',
|
||||
w: annotationWidth,
|
||||
h: annotationHeight,
|
||||
size: 'm',
|
||||
growY: 0,
|
||||
align: 'middle',
|
||||
geo: 'rectangle',
|
||||
verticalAlign: 'middle',
|
||||
labelColor: 'black',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
annotationInfo = {
|
||||
x: annotationInfo.x,
|
||||
isLocked: annotationInfo.isLocked,
|
||||
y: annotationInfo.y,
|
||||
rotation: annotationInfo.rotation,
|
||||
typeName: annotationInfo.typeName,
|
||||
opacity: annotationInfo.opacity,
|
||||
parentId: annotationInfo.parentId,
|
||||
index: annotationInfo.index,
|
||||
id: annotationInfo.id,
|
||||
meta: annotationInfo.meta,
|
||||
type: 'geo',
|
||||
props: {
|
||||
url: '',
|
||||
text: annotationInfo.props.text,
|
||||
color: annotationInfo.props.color,
|
||||
font: annotationInfo.props.font,
|
||||
fill: annotationInfo.props.fill,
|
||||
dash: annotationInfo.props.dash,
|
||||
h: annotationInfo.props.h,
|
||||
w: annotationInfo.props.w,
|
||||
size: annotationInfo.props.size,
|
||||
growY: 0,
|
||||
align: 'middle',
|
||||
geo: annotationInfo.props.geo,
|
||||
verticalAlign: 'middle',
|
||||
labelColor: annotationInfo.props.labelColor,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const cpg = parseInt(annotationInfo?.id?.split?.('/')?.[1], 10);
|
||||
if (cpg !== parseInt(curPageId, 10)) return;
|
||||
|
||||
annotationInfo.questionType = false;
|
||||
}
|
||||
result[annotationInfo.id] = annotationInfo;
|
||||
|
@ -101,7 +101,6 @@ const TldrawV2GlobalStyle = createGlobalStyle`
|
||||
.tl-collaborator__cursor {
|
||||
height: auto !important;
|
||||
width: auto !important;
|
||||
transition: transform 0.25s ease-out !important;
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -111,7 +111,9 @@ class Settings {
|
||||
const { status } = Meteor.status();
|
||||
if (status === 'connected') {
|
||||
c.stop();
|
||||
mutation(userSettings);
|
||||
if (typeof mutation === 'function') {
|
||||
mutation(userSettings);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
6
bigbluebutton-html5/package-lock.json
generated
6
bigbluebutton-html5/package-lock.json
generated
@ -3418,9 +3418,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"bigbluebutton-html-plugin-sdk": {
|
||||
"version": "0.0.35",
|
||||
"resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.35.tgz",
|
||||
"integrity": "sha512-KD38ThSmr8JfitpjXWTas5SoKuNM4j++G36H7jez4jdvQLz3AJLJAvSGfJrqjnxAwS4oQU5V2Z5bfKrR1a14Vg==",
|
||||
"version": "0.0.37",
|
||||
"resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.37.tgz",
|
||||
"integrity": "sha512-3zkM22DYkElg+cZTMMt+fB3xLPHG4Rj0lJvagRRJNUw+BTWJWnTE/CP8u51HVuU+P6rTnJIiPFGddigDr7Pk3A==",
|
||||
"requires": {
|
||||
"@apollo/client": "^3.8.7"
|
||||
}
|
||||
|
@ -47,7 +47,7 @@
|
||||
"autoprefixer": "^10.4.4",
|
||||
"axios": "^1.6.4",
|
||||
"babel-runtime": "~6.26.0",
|
||||
"bigbluebutton-html-plugin-sdk": "0.0.35",
|
||||
"bigbluebutton-html-plugin-sdk": "0.0.37",
|
||||
"bowser": "^2.11.0",
|
||||
"browser-bunyan": "^1.8.0",
|
||||
"classnames": "^2.2.6",
|
||||
|
@ -942,7 +942,7 @@ def BBB_server_standalone(hostname, x=100, y=300):
|
||||
install_options.append('-g')
|
||||
|
||||
install_options_str = ' '.join(install_options)
|
||||
user_data['runcmd'].append(f'sudo -u ubuntu RELEASE="{args.release}" INSTALL_OPTIONS="{install_options_str}" /testserver.sh')
|
||||
user_data['runcmd'].append(f'runuser -u ubuntu RELEASE="{args.release}" INSTALL_OPTIONS="{install_options_str}" /testserver.sh')
|
||||
|
||||
if notification_url:
|
||||
user_data['phone_home'] = {'url': notification_url, 'tries': 1}
|
||||
|
@ -124,7 +124,7 @@ class ConnectionController {
|
||||
builder {
|
||||
"response" "authorized"
|
||||
"X-Hasura-Role" "not_joined_bbb_client"
|
||||
"X-Hasura-ModeratorInMeeting" ""
|
||||
"X-Hasura-ModeratorInMeeting" removedUserSession.isModerator() ? removedUserSession.meetingId : ""
|
||||
"X-Hasura-PresenterInMeeting" ""
|
||||
"X-Hasura-UserId" removedUserSession.userId
|
||||
"X-Hasura-MeetingId" removedUserSession.meetingId
|
||||
|
@ -5,19 +5,19 @@ case "$1" in
|
||||
|
||||
fc-cache -f
|
||||
|
||||
sudo -u postgres psql -c "alter user postgres password 'bbb_graphql'"
|
||||
sudo -u postgres psql -c "drop database if exists bbb_graphql with (force)"
|
||||
sudo -u postgres psql -c "create database bbb_graphql WITH TEMPLATE template0 LC_COLLATE 'C.UTF-8'"
|
||||
sudo -u postgres psql -c "alter database bbb_graphql set timezone to 'UTC'"
|
||||
sudo -u postgres psql -U postgres -d bbb_graphql -q -f /usr/share/bbb-graphql-server/bbb_schema.sql --set ON_ERROR_STOP=on
|
||||
runuser -u postgres -- psql -c "alter user postgres password 'bbb_graphql'"
|
||||
runuser -u postgres -- psql -c "drop database if exists bbb_graphql with (force)"
|
||||
runuser -u postgres -- psql -c "create database bbb_graphql WITH TEMPLATE template0 LC_COLLATE 'C.UTF-8'"
|
||||
runuser -u postgres -- psql -c "alter database bbb_graphql set timezone to 'UTC'"
|
||||
runuser -u postgres -- psql -U postgres -d bbb_graphql -q -f /usr/share/bbb-graphql-server/bbb_schema.sql --set ON_ERROR_STOP=on
|
||||
|
||||
DATABASE_NAME="hasura_app"
|
||||
DB_EXISTS=$(sudo -u postgres psql -U postgres -tAc "SELECT 1 FROM pg_database WHERE datname='$DATABASE_NAME'")
|
||||
DB_EXISTS=$(runuser -u postgres -- psql -U postgres -tAc "SELECT 1 FROM pg_database WHERE datname='$DATABASE_NAME'")
|
||||
if [ "$DB_EXISTS" = '1' ]
|
||||
then
|
||||
echo "Database $DATABASE_NAME already exists"
|
||||
else
|
||||
sudo -u postgres psql -c "create database hasura_app"
|
||||
runuser -u postgres -- psql -c "create database hasura_app"
|
||||
echo "Database $DATABASE_NAME created"
|
||||
fi
|
||||
|
||||
|
@ -344,7 +344,7 @@ const createEndpointTableData = [
|
||||
"name": "disabledFeatures",
|
||||
"required": false,
|
||||
"type": "String",
|
||||
"description": (<>List (comma-separated) of features to disable in a particular meeting. (added 2.5)<br /><br />Available options to disable:<br /><ul><li><code className="language-plaintext highlighter-rouge">breakoutRooms</code>- <b>Breakout Rooms</b> </li><li><code className="language-plaintext highlighter-rouge">captions</code>- <b>Closed Caption</b> </li><li><code className="language-plaintext highlighter-rouge">chat</code>- <b>Chat</b></li><li><code className="language-plaintext highlighter-rouge">downloadPresentationWithAnnotations</code>- <b>Annotated presentation download</b></li><li><code className="language-plaintext highlighter-rouge">snapshotOfCurrentSlide</code>- <b>Allow snapshot of the current slide</b></li><li><code className="language-plaintext highlighter-rouge">externalVideos</code>- <b>Share an external video</b> </li><li><code className="language-plaintext highlighter-rouge">importPresentationWithAnnotationsFromBreakoutRooms</code>- <b>Capture breakout presentation</b></li><li><code className="language-plaintext highlighter-rouge">importSharedNotesFromBreakoutRooms</code>- <b>Capture breakout shared notes</b></li><li><code className="language-plaintext highlighter-rouge">layouts</code>- <b>Layouts</b> (allow only default layout)</li><li><code className="language-plaintext highlighter-rouge">learningDashboard</code>- <b>Learning Analytics Dashboard</b></li><li><code className="language-plaintext highlighter-rouge">polls</code>- <b>Polls</b> </li><li><code className="language-plaintext highlighter-rouge">screenshare</code>- <b>Screen Sharing</b></li><li><code className="language-plaintext highlighter-rouge">sharedNotes</code>- <b>Shared Notes</b></li><li><code className="language-plaintext highlighter-rouge">virtualBackgrounds</code>- <b>Virtual Backgrounds</b></li><li><code className="language-plaintext highlighter-rouge">customVirtualBackgrounds</code>- <b>Virtual Backgrounds Upload</b></li><li><code className="language-plaintext highlighter-rouge">liveTranscription</code>- <b>Live Transcription</b></li><li><code className="language-plaintext highlighter-rouge">presentation</code>- <b>Presentation</b></li><li><code className="language-plaintext highlighter-rouge">cameraAsContent</code>-<b>Enables/Disables camera as a content</b></li><li><code className="language-plaintext highlighter-rouge">timer</code>- <b>disables timer</b></li></ul></>)
|
||||
"description": (<>List (comma-separated) of features to disable in a particular meeting. (added 2.5)<br /><br />Available options to disable:<br /><ul><li><code className="language-plaintext highlighter-rouge">breakoutRooms</code>- <b>Breakout Rooms</b> </li><li><code className="language-plaintext highlighter-rouge">captions</code>- <b>Closed Caption</b> </li><li><code className="language-plaintext highlighter-rouge">chat</code>- <b>Chat</b></li><li><code className="language-plaintext highlighter-rouge">downloadPresentationWithAnnotations</code>- <b>Annotated presentation download</b></li><li><code className="language-plaintext highlighter-rouge">snapshotOfCurrentSlide</code>- <b>Allow snapshot of the current slide</b></li><li><code className="language-plaintext highlighter-rouge">externalVideos</code>- <b>Share an external video</b> </li><li><code className="language-plaintext highlighter-rouge">importPresentationWithAnnotationsFromBreakoutRooms</code>- <b>Capture breakout presentation</b></li><li><code className="language-plaintext highlighter-rouge">importSharedNotesFromBreakoutRooms</code>- <b>Capture breakout shared notes</b></li><li><code className="language-plaintext highlighter-rouge">layouts</code>- <b>Layouts</b> (allow only default layout)</li><li><code className="language-plaintext highlighter-rouge">learningDashboard</code>- <b>Learning Analytics Dashboard</b></li><li><code className="language-plaintext highlighter-rouge">learningDashboardDownloadSessionData</code>- <b>Learning Analytics Dashboard Download Session Data (prevents the option to download)</b></li><li><code className="language-plaintext highlighter-rouge">polls</code>- <b>Polls</b> </li><li><code className="language-plaintext highlighter-rouge">screenshare</code>- <b>Screen Sharing</b></li><li><code className="language-plaintext highlighter-rouge">sharedNotes</code>- <b>Shared Notes</b></li><li><code className="language-plaintext highlighter-rouge">virtualBackgrounds</code>- <b>Virtual Backgrounds</b></li><li><code className="language-plaintext highlighter-rouge">customVirtualBackgrounds</code>- <b>Virtual Backgrounds Upload</b></li><li><code className="language-plaintext highlighter-rouge">liveTranscription</code>- <b>Live Transcription</b></li><li><code className="language-plaintext highlighter-rouge">presentation</code>- <b>Presentation</b></li><li><code className="language-plaintext highlighter-rouge">cameraAsContent</code>-<b>Enables/Disables camera as a content</b></li><li><code className="language-plaintext highlighter-rouge">timer</code>- <b>disables timer</b></li></ul></>)
|
||||
},
|
||||
{
|
||||
"name": "disabledFeaturesExclude",
|
||||
|
@ -104,7 +104,7 @@ Updated in 2.6:
|
||||
|
||||
Updated in 2.7:
|
||||
|
||||
- **create** - **Added:** `preUploadedPresentation`, `preUploadedPresentationName`, `disabledFeatures` options`cameraAsContent`, `snapshotOfCurrentSlide`, `downloadPresentationOriginalFile`, `downloadPresentationConvertedToPdf`, `timer`.
|
||||
- **create** - **Added:** `preUploadedPresentation`, `preUploadedPresentationName`, `disabledFeatures` options`cameraAsContent`, `snapshotOfCurrentSlide`, `downloadPresentationOriginalFile`, `downloadPresentationConvertedToPdf`, `timer`, `learningDashboardDownloadSessionData` (2.7.5).
|
||||
- **join** - **Added:** `redirectErrorUrl`, `userdata-bbb_fullaudio_bridge`
|
||||
|
||||
Updated in 3.0:
|
||||
|
Loading…
Reference in New Issue
Block a user