Merge pull request #15894 from JoVictorNunes/shared-notes-on-media
This commit is contained in:
commit
50010ea528
@ -0,0 +1,38 @@
|
|||||||
|
package org.bigbluebutton.core.apps.pads
|
||||||
|
|
||||||
|
import org.bigbluebutton.common2.msgs._
|
||||||
|
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||||
|
import org.bigbluebutton.core.bus.MessageBus
|
||||||
|
import org.bigbluebutton.core.models.Pads
|
||||||
|
import org.bigbluebutton.core.running.LiveMeeting
|
||||||
|
|
||||||
|
trait PadPinnedReqMsgHdlr extends RightsManagementTrait {
|
||||||
|
this: PadsApp2x =>
|
||||||
|
|
||||||
|
def handle(msg: PadPinnedReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||||
|
|
||||||
|
def broadcastEvent(groupId: String, pinned: Boolean): Unit = {
|
||||||
|
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||||
|
val envelope = BbbCoreEnvelope(PadPinnedEvtMsg.NAME, routing)
|
||||||
|
val header = BbbClientMsgHeader(PadPinnedEvtMsg.NAME, liveMeeting.props.meetingProp.intId, "not-used")
|
||||||
|
val body = PadPinnedEvtMsgBody(groupId, pinned)
|
||||||
|
val event = PadPinnedEvtMsg(header, body)
|
||||||
|
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||||
|
|
||||||
|
bus.outGW.send(msgEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Pads.hasAccess(liveMeeting, msg.body.externalId, msg.header.userId)) {
|
||||||
|
if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
||||||
|
val meetingId = liveMeeting.props.meetingProp.intId
|
||||||
|
val reason = "You need to be the presenter to pin Shared Notes"
|
||||||
|
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||||
|
} else {
|
||||||
|
Pads.getGroup(liveMeeting.pads, msg.body.externalId) match {
|
||||||
|
case Some(group) => broadcastEvent(group.externalId, msg.body.pinned)
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,7 @@ class PadsApp2x(implicit val context: ActorContext)
|
|||||||
with PadUpdatedSysMsgHdlr
|
with PadUpdatedSysMsgHdlr
|
||||||
with PadContentSysMsgHdlr
|
with PadContentSysMsgHdlr
|
||||||
with PadPatchSysMsgHdlr
|
with PadPatchSysMsgHdlr
|
||||||
with PadUpdatePubMsgHdlr {
|
with PadUpdatePubMsgHdlr
|
||||||
|
with PadPinnedReqMsgHdlr {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -175,6 +175,8 @@ class ReceivedJsonMsgHandlerActor(
|
|||||||
routeGenericMsg[PadUpdatePubMsg](envelope, jsonNode)
|
routeGenericMsg[PadUpdatePubMsg](envelope, jsonNode)
|
||||||
case PadCapturePubMsg.NAME =>
|
case PadCapturePubMsg.NAME =>
|
||||||
routePadMsg[PadCapturePubMsg](envelope, jsonNode)
|
routePadMsg[PadCapturePubMsg](envelope, jsonNode)
|
||||||
|
case PadPinnedReqMsg.NAME =>
|
||||||
|
routeGenericMsg[PadPinnedReqMsg](envelope, jsonNode)
|
||||||
|
|
||||||
// Voice
|
// Voice
|
||||||
case RecordingStartedVoiceConfEvtMsg.NAME =>
|
case RecordingStartedVoiceConfEvtMsg.NAME =>
|
||||||
|
@ -492,6 +492,7 @@ class MeetingActor(
|
|||||||
case m: PadContentSysMsg => padsApp2x.handle(m, liveMeeting, msgBus)
|
case m: PadContentSysMsg => padsApp2x.handle(m, liveMeeting, msgBus)
|
||||||
case m: PadPatchSysMsg => padsApp2x.handle(m, liveMeeting, msgBus)
|
case m: PadPatchSysMsg => padsApp2x.handle(m, liveMeeting, msgBus)
|
||||||
case m: PadUpdatePubMsg => padsApp2x.handle(m, liveMeeting, msgBus)
|
case m: PadUpdatePubMsg => padsApp2x.handle(m, liveMeeting, msgBus)
|
||||||
|
case m: PadPinnedReqMsg => padsApp2x.handle(m, liveMeeting, msgBus)
|
||||||
|
|
||||||
// Lock Settings
|
// Lock Settings
|
||||||
case m: ChangeLockSettingsInMeetingCmdMsg =>
|
case m: ChangeLockSettingsInMeetingCmdMsg =>
|
||||||
|
@ -118,3 +118,13 @@ case class PadUpdateCmdMsgBody(groupId: String, name: String, text: String)
|
|||||||
object PadCapturePubMsg { val NAME = "PadCapturePubMsg" }
|
object PadCapturePubMsg { val NAME = "PadCapturePubMsg" }
|
||||||
case class PadCapturePubMsg(header: BbbCoreHeaderWithMeetingId, body: PadCapturePubMsgBody) extends PadStandardMsg
|
case class PadCapturePubMsg(header: BbbCoreHeaderWithMeetingId, body: PadCapturePubMsgBody) extends PadStandardMsg
|
||||||
case class PadCapturePubMsgBody(parentMeetingId: String, breakoutId: String, padId: String, meetingName: String, sequence: Int)
|
case class PadCapturePubMsgBody(parentMeetingId: String, breakoutId: String, padId: String, meetingName: String, sequence: Int)
|
||||||
|
|
||||||
|
// client -> apps
|
||||||
|
object PadPinnedReqMsg { val NAME = "PadPinnedReqMsg" }
|
||||||
|
case class PadPinnedReqMsg(header: BbbClientMsgHeader, body: PadPinnedReqMsgBody) extends StandardMsg
|
||||||
|
case class PadPinnedReqMsgBody(externalId: String, pinned: Boolean)
|
||||||
|
|
||||||
|
// apps -> client
|
||||||
|
object PadPinnedEvtMsg { val NAME = "PadPinnedEvtMsg" }
|
||||||
|
case class PadPinnedEvtMsg(header: BbbClientMsgHeader, body: PadPinnedEvtMsgBody) extends BbbCoreMsg
|
||||||
|
case class PadPinnedEvtMsgBody(externalId: String, pinned: Boolean)
|
||||||
|
@ -7,6 +7,7 @@ import padContent from './handlers/padContent';
|
|||||||
import padTail from './handlers/padTail';
|
import padTail from './handlers/padTail';
|
||||||
import sessionDeleted from './handlers/sessionDeleted';
|
import sessionDeleted from './handlers/sessionDeleted';
|
||||||
import captureSharedNotes from './handlers/captureSharedNotes';
|
import captureSharedNotes from './handlers/captureSharedNotes';
|
||||||
|
import padPinned from './handlers/padPinned';
|
||||||
|
|
||||||
RedisPubSub.on('PadGroupCreatedRespMsg', groupCreated);
|
RedisPubSub.on('PadGroupCreatedRespMsg', groupCreated);
|
||||||
RedisPubSub.on('PadCreatedRespMsg', padCreated);
|
RedisPubSub.on('PadCreatedRespMsg', padCreated);
|
||||||
@ -16,3 +17,4 @@ RedisPubSub.on('PadContentEvtMsg', padContent);
|
|||||||
RedisPubSub.on('PadTailEvtMsg', padTail);
|
RedisPubSub.on('PadTailEvtMsg', padTail);
|
||||||
RedisPubSub.on('PadSessionDeletedEvtMsg', sessionDeleted);
|
RedisPubSub.on('PadSessionDeletedEvtMsg', sessionDeleted);
|
||||||
RedisPubSub.on('CaptureSharedNotesReqEvtMsg', captureSharedNotes);
|
RedisPubSub.on('CaptureSharedNotesReqEvtMsg', captureSharedNotes);
|
||||||
|
RedisPubSub.on('PadPinnedEvtMsg', padPinned);
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import pinPad from '/imports/api/pads/server/modifiers/pinPad';
|
||||||
|
|
||||||
|
export default function padPinned({ header, body }) {
|
||||||
|
const {
|
||||||
|
meetingId,
|
||||||
|
} = header;
|
||||||
|
|
||||||
|
const {
|
||||||
|
externalId,
|
||||||
|
pinned,
|
||||||
|
} = body;
|
||||||
|
|
||||||
|
pinPad(meetingId, externalId, pinned);
|
||||||
|
}
|
@ -2,9 +2,11 @@ import { Meteor } from 'meteor/meteor';
|
|||||||
import createGroup from './methods/createGroup';
|
import createGroup from './methods/createGroup';
|
||||||
import createSession from './methods/createSession';
|
import createSession from './methods/createSession';
|
||||||
import getPadId from './methods/getPadId';
|
import getPadId from './methods/getPadId';
|
||||||
|
import pinPad from './methods/pinPad';
|
||||||
|
|
||||||
Meteor.methods({
|
Meteor.methods({
|
||||||
createGroup,
|
createGroup,
|
||||||
createSession,
|
createSession,
|
||||||
getPadId,
|
getPadId,
|
||||||
|
pinPad,
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
import RedisPubSub from '/imports/startup/server/redis';
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import { check } from 'meteor/check';
|
||||||
|
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||||
|
import Logger from '/imports/startup/server/logger';
|
||||||
|
|
||||||
|
export default function pinPad(externalId, pinned) {
|
||||||
|
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||||
|
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||||
|
const EVENT_NAME = 'PadPinnedReqMsg';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||||
|
|
||||||
|
check(meetingId, String);
|
||||||
|
check(requesterUserId, String);
|
||||||
|
check(externalId, String);
|
||||||
|
check(pinned, Boolean);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
externalId,
|
||||||
|
pinned,
|
||||||
|
};
|
||||||
|
|
||||||
|
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error(`Exception while invoking method pinPad ${err.stack}`);
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@ export default function createPad(meetingId, externalId, padId) {
|
|||||||
const modifier = {
|
const modifier = {
|
||||||
$set: {
|
$set: {
|
||||||
padId,
|
padId,
|
||||||
|
pinned: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
import { check } from 'meteor/check';
|
||||||
|
import { default as Pads } from '/imports/api/pads';
|
||||||
|
import Logger from '/imports/startup/server/logger';
|
||||||
|
|
||||||
|
export default function pinPad(meetingId, externalId, pinned) {
|
||||||
|
try {
|
||||||
|
check(meetingId, String);
|
||||||
|
check(externalId, String);
|
||||||
|
check(pinned, Boolean);
|
||||||
|
|
||||||
|
if (pinned) {
|
||||||
|
Pads.update({ meetingId, pinned: true }, { $set: { pinned: false } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const selector = {
|
||||||
|
meetingId,
|
||||||
|
externalId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const modifier = {
|
||||||
|
$set: {
|
||||||
|
pinned,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const numberAffected = Pads.update(selector, modifier);
|
||||||
|
|
||||||
|
if (numberAffected) {
|
||||||
|
const prefix = pinned ? '' : 'un';
|
||||||
|
Logger.debug(`Pad ${prefix}pinned external=${externalId} meeting=${meetingId}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error(`Pinning pad: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
@ -47,6 +47,7 @@ import Notifications from '../notifications/container';
|
|||||||
import GlobalStyles from '/imports/ui/stylesheets/styled-components/globalStyles';
|
import GlobalStyles from '/imports/ui/stylesheets/styled-components/globalStyles';
|
||||||
import ActionsBarContainer from '../actions-bar/container';
|
import ActionsBarContainer from '../actions-bar/container';
|
||||||
import PushLayoutEngine from '../layout/push-layout/pushLayoutEngine';
|
import PushLayoutEngine from '../layout/push-layout/pushLayoutEngine';
|
||||||
|
import NotesContainer from '/imports/ui/components/notes/container';
|
||||||
|
|
||||||
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
|
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
|
||||||
const APP_CONFIG = Meteor.settings.public.app;
|
const APP_CONFIG = Meteor.settings.public.app;
|
||||||
@ -479,6 +480,7 @@ class App extends Component {
|
|||||||
shouldShowPresentation,
|
shouldShowPresentation,
|
||||||
shouldShowScreenshare,
|
shouldShowScreenshare,
|
||||||
shouldShowExternalVideo,
|
shouldShowExternalVideo,
|
||||||
|
shouldShowSharedNotes,
|
||||||
isPresenter,
|
isPresenter,
|
||||||
selectedLayout,
|
selectedLayout,
|
||||||
presentationIsOpen,
|
presentationIsOpen,
|
||||||
@ -513,6 +515,7 @@ class App extends Component {
|
|||||||
? <ExternalVideoContainer isLayoutSwapped={!presentationIsOpen} isPresenter={isPresenter} />
|
? <ExternalVideoContainer isLayoutSwapped={!presentationIsOpen} isPresenter={isPresenter} />
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
{shouldShowSharedNotes ? <NotesContainer area="media" layoutType={selectedLayout} /> : null}
|
||||||
{this.renderCaptions()}
|
{this.renderCaptions()}
|
||||||
<AudioCaptionsSpeechContainer />
|
<AudioCaptionsSpeechContainer />
|
||||||
{this.renderAudioCaptions()}
|
{this.renderAudioCaptions()}
|
||||||
|
@ -254,6 +254,7 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
|
|||||||
const AppSettings = Settings.application;
|
const AppSettings = Settings.application;
|
||||||
const { selectedLayout, pushLayout } = AppSettings;
|
const { selectedLayout, pushLayout } = AppSettings;
|
||||||
const { viewScreenshare } = Settings.dataSaving;
|
const { viewScreenshare } = Settings.dataSaving;
|
||||||
|
const shouldShowSharedNotes = MediaService.shouldShowSharedNotes();
|
||||||
const shouldShowExternalVideo = MediaService.shouldShowExternalVideo();
|
const shouldShowExternalVideo = MediaService.shouldShowExternalVideo();
|
||||||
const shouldShowScreenshare = MediaService.shouldShowScreenshare()
|
const shouldShowScreenshare = MediaService.shouldShowScreenshare()
|
||||||
&& (viewScreenshare || currentUser?.presenter);
|
&& (viewScreenshare || currentUser?.presenter);
|
||||||
@ -298,8 +299,9 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
|
|||||||
pushAlertEnabled: AppSettings.chatPushAlerts,
|
pushAlertEnabled: AppSettings.chatPushAlerts,
|
||||||
darkTheme: AppSettings.darkTheme,
|
darkTheme: AppSettings.darkTheme,
|
||||||
shouldShowScreenshare,
|
shouldShowScreenshare,
|
||||||
shouldShowPresentation: !shouldShowScreenshare && !shouldShowExternalVideo,
|
shouldShowPresentation: !shouldShowScreenshare && !shouldShowExternalVideo && !shouldShowSharedNotes,
|
||||||
shouldShowExternalVideo,
|
shouldShowExternalVideo,
|
||||||
|
shouldShowSharedNotes,
|
||||||
isLargeFont: Session.get('isLargeFont'),
|
isLargeFont: Session.get('isLargeFont'),
|
||||||
presentationRestoreOnUpdate: getFromUserSettings(
|
presentationRestoreOnUpdate: getFromUserSettings(
|
||||||
'bbb_force_restore_presentation_on_new_events',
|
'bbb_force_restore_presentation_on_new_events',
|
||||||
|
@ -9,6 +9,7 @@ const Header = ({
|
|||||||
rightButtonProps,
|
rightButtonProps,
|
||||||
customRightButton,
|
customRightButton,
|
||||||
'data-test': dataTest,
|
'data-test': dataTest,
|
||||||
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const renderCloseButton = () => (
|
const renderCloseButton = () => (
|
||||||
<Right {...rightButtonProps} />
|
<Right {...rightButtonProps} />
|
||||||
@ -21,8 +22,8 @@ const Header = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Styled.Header data-test={dataTest ? dataTest : ''}>
|
<Styled.Header data-test={dataTest ? dataTest : ''} {...rest}>
|
||||||
<Left {...leftButtonProps} />
|
{leftButtonProps ? <Left {...leftButtonProps} /> : <div />}
|
||||||
{customRightButton
|
{customRightButton
|
||||||
? renderCustomRightButton()
|
? renderCustomRightButton()
|
||||||
: rightButtonProps
|
: rightButtonProps
|
||||||
|
@ -3,6 +3,7 @@ import Auth from '/imports/ui/services/auth';
|
|||||||
|
|
||||||
import { getStreamer } from '/imports/api/external-videos';
|
import { getStreamer } from '/imports/api/external-videos';
|
||||||
import { makeCall } from '/imports/ui/services/api';
|
import { makeCall } from '/imports/ui/services/api';
|
||||||
|
import NotesService from '/imports/ui/components/notes/service';
|
||||||
|
|
||||||
import ReactPlayer from 'react-player';
|
import ReactPlayer from 'react-player';
|
||||||
|
|
||||||
@ -29,6 +30,10 @@ const startWatching = (url) => {
|
|||||||
} else if (Panopto.canPlay(url)) {
|
} else if (Panopto.canPlay(url)) {
|
||||||
externalVideoUrl = Panopto.getSocialUrl(url);
|
externalVideoUrl = Panopto.getSocialUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close Shared Notes if open.
|
||||||
|
NotesService.pinSharedNotes(false);
|
||||||
|
|
||||||
makeCall('startWatchingExternalVideo', externalVideoUrl);
|
makeCall('startWatchingExternalVideo', externalVideoUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1149,6 +1149,55 @@ const reducer = (state, action) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTES
|
||||||
|
case ACTIONS.SET_SHARED_NOTES_OUTPUT: {
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
} = action.value;
|
||||||
|
const { sharedNotes } = state.output;
|
||||||
|
if (sharedNotes.width === width
|
||||||
|
&& sharedNotes.height === height
|
||||||
|
&& sharedNotes.top === top
|
||||||
|
&& sharedNotes.left === left
|
||||||
|
&& sharedNotes.right === right) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
output: {
|
||||||
|
...state.output,
|
||||||
|
sharedNotes: {
|
||||||
|
...sharedNotes,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case ACTIONS.SET_NOTES_IS_PINNED: {
|
||||||
|
const { sharedNotes } = state.input;
|
||||||
|
if (sharedNotes.isPinned === action.value) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
input: {
|
||||||
|
...state.input,
|
||||||
|
sharedNotes: {
|
||||||
|
...sharedNotes,
|
||||||
|
isPinned: action.value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error('Unexpected action');
|
throw new Error('Unexpected action');
|
||||||
}
|
}
|
||||||
|
@ -94,6 +94,9 @@ export const ACTIONS = {
|
|||||||
SET_HAS_EXTERNAL_VIDEO: 'setHasExternalVideo',
|
SET_HAS_EXTERNAL_VIDEO: 'setHasExternalVideo',
|
||||||
SET_EXTERNAL_VIDEO_SIZE: 'setExternalVideoSize',
|
SET_EXTERNAL_VIDEO_SIZE: 'setExternalVideoSize',
|
||||||
SET_EXTERNAL_VIDEO_OUTPUT: 'setExternalVideoOutput',
|
SET_EXTERNAL_VIDEO_OUTPUT: 'setExternalVideoOutput',
|
||||||
|
|
||||||
|
SET_SHARED_NOTES_OUTPUT: 'setSharedNotesOutput',
|
||||||
|
SET_NOTES_IS_PINNED: 'setNotesIsPinned',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PANELS = {
|
export const PANELS = {
|
||||||
|
@ -95,6 +95,13 @@ export const INITIAL_INPUT_STATE = {
|
|||||||
browserWidth: 0,
|
browserWidth: 0,
|
||||||
browserHeight: 0,
|
browserHeight: 0,
|
||||||
},
|
},
|
||||||
|
sharedNotes: {
|
||||||
|
isPinned: false,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
browserWidth: 0,
|
||||||
|
browserHeight: 0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const INITIAL_OUTPUT_STATE = {
|
export const INITIAL_OUTPUT_STATE = {
|
||||||
@ -227,4 +234,13 @@ export const INITIAL_OUTPUT_STATE = {
|
|||||||
tabOrder: 0,
|
tabOrder: 0,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
|
sharedNotes: {
|
||||||
|
display: false,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
tabOrder: 0,
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -683,6 +683,17 @@ const CustomLayout = (props) => {
|
|||||||
right: isRTL ? (mediaBounds.right + horizontalCameraDiff) : null,
|
right: isRTL ? (mediaBounds.right + horizontalCameraDiff) : null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
layoutContextDispatch({
|
||||||
|
type: ACTIONS.SET_SHARED_NOTES_OUTPUT,
|
||||||
|
value: {
|
||||||
|
width: mediaBounds.width,
|
||||||
|
height: mediaBounds.height,
|
||||||
|
top: mediaBounds.top,
|
||||||
|
left: mediaBounds.left,
|
||||||
|
right: isRTL ? (mediaBounds.right + horizontalCameraDiff) : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -470,6 +470,17 @@ const PresentationFocusLayout = (props) => {
|
|||||||
right: mediaBounds.right,
|
right: mediaBounds.right,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
layoutContextDispatch({
|
||||||
|
type: ACTIONS.SET_SHARED_NOTES_OUTPUT,
|
||||||
|
value: {
|
||||||
|
width: isOpen ? mediaBounds.width : 0,
|
||||||
|
height: isOpen ? mediaBounds.height : 0,
|
||||||
|
top: mediaBounds.top,
|
||||||
|
left: mediaBounds.left,
|
||||||
|
right: mediaBounds.right,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -536,6 +536,17 @@ const SmartLayout = (props) => {
|
|||||||
right: isRTL ? (mediaBounds.right + horizontalCameraDiff) : null,
|
right: isRTL ? (mediaBounds.right + horizontalCameraDiff) : null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
layoutContextDispatch({
|
||||||
|
type: ACTIONS.SET_SHARED_NOTES_OUTPUT,
|
||||||
|
value: {
|
||||||
|
width: mediaBounds.width,
|
||||||
|
height: mediaBounds.height,
|
||||||
|
top: mediaBounds.top,
|
||||||
|
left: mediaBounds.left,
|
||||||
|
right: isRTL ? (mediaBounds.right + horizontalCameraDiff) : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -491,6 +491,17 @@ const VideoFocusLayout = (props) => {
|
|||||||
right: isRTL ? mediaBounds.right : null,
|
right: isRTL ? mediaBounds.right : null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
layoutContextDispatch({
|
||||||
|
type: ACTIONS.SET_SHARED_NOTES_OUTPUT,
|
||||||
|
value: {
|
||||||
|
width: mediaBounds.width,
|
||||||
|
height: mediaBounds.height,
|
||||||
|
top: mediaBounds.top,
|
||||||
|
left: mediaBounds.left,
|
||||||
|
right: isRTL ? mediaBounds.right : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -6,6 +6,7 @@ import getFromUserSettings from '/imports/ui/services/users-settings';
|
|||||||
import { isExternalVideoEnabled, isScreenSharingEnabled } from '/imports/ui/services/features';
|
import { isExternalVideoEnabled, isScreenSharingEnabled } from '/imports/ui/services/features';
|
||||||
import { ACTIONS } from '../layout/enums';
|
import { ACTIONS } from '../layout/enums';
|
||||||
import UserService from '/imports/ui/components/user-list/service';
|
import UserService from '/imports/ui/components/user-list/service';
|
||||||
|
import NotesService from '/imports/ui/components/notes/service';
|
||||||
|
|
||||||
const LAYOUT_CONFIG = Meteor.settings.public.layout;
|
const LAYOUT_CONFIG = Meteor.settings.public.layout;
|
||||||
const KURENTO_CONFIG = Meteor.settings.public.kurento;
|
const KURENTO_CONFIG = Meteor.settings.public.kurento;
|
||||||
@ -34,6 +35,10 @@ function shouldShowExternalVideo() {
|
|||||||
return isExternalVideoEnabled() && getVideoUrl();
|
return isExternalVideoEnabled() && getVideoUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldShowSharedNotes() {
|
||||||
|
return NotesService.isSharedNotesPinned();
|
||||||
|
}
|
||||||
|
|
||||||
function shouldShowOverlay() {
|
function shouldShowOverlay() {
|
||||||
return getFromUserSettings('bbb_enable_video', KURENTO_CONFIG.enableVideo);
|
return getFromUserSettings('bbb_enable_video', KURENTO_CONFIG.enableVideo);
|
||||||
}
|
}
|
||||||
@ -53,4 +58,5 @@ export default {
|
|||||||
shouldShowOverlay,
|
shouldShowOverlay,
|
||||||
isVideoBroadcasting,
|
isVideoBroadcasting,
|
||||||
setPresentationIsOpen,
|
setPresentationIsOpen,
|
||||||
|
shouldShowSharedNotes,
|
||||||
};
|
};
|
||||||
|
@ -42,11 +42,13 @@ const NavBarContainer = ({ children, ...props }) => {
|
|||||||
const sidebarNavigation = layoutSelectInput((i) => i.sidebarNavigation);
|
const sidebarNavigation = layoutSelectInput((i) => i.sidebarNavigation);
|
||||||
const navBar = layoutSelectOutput((i) => i.navBar);
|
const navBar = layoutSelectOutput((i) => i.navBar);
|
||||||
const layoutContextDispatch = layoutDispatch();
|
const layoutContextDispatch = layoutDispatch();
|
||||||
|
const sharedNotes = layoutSelectInput((i) => i.sharedNotes);
|
||||||
|
const { isPinned: notesIsPinned } = sharedNotes;
|
||||||
|
|
||||||
const { sidebarContentPanel } = sidebarContent;
|
const { sidebarContentPanel } = sidebarContent;
|
||||||
const { sidebarNavPanel } = sidebarNavigation;
|
const { sidebarNavPanel } = sidebarNavigation;
|
||||||
|
|
||||||
const hasUnreadNotes = sidebarContentPanel !== PANELS.SHARED_NOTES && unread;
|
const hasUnreadNotes = sidebarContentPanel !== PANELS.SHARED_NOTES && unread && !notesIsPinned;
|
||||||
const hasUnreadMessages = checkUnreadMessages(
|
const hasUnreadMessages = checkUnreadMessages(
|
||||||
{ groupChatsMessages, groupChats, users: users[Auth.meetingID] },
|
{ groupChatsMessages, groupChats, users: users[Auth.meetingID] },
|
||||||
);
|
);
|
||||||
|
@ -4,11 +4,14 @@ import { defineMessages, injectIntl } from 'react-intl';
|
|||||||
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
|
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
|
||||||
import Service from '/imports/ui/components/notes/service';
|
import Service from '/imports/ui/components/notes/service';
|
||||||
import PadContainer from '/imports/ui/components/pads/container';
|
import PadContainer from '/imports/ui/components/pads/container';
|
||||||
import ConverterButtonContainer from './converter-button/container';
|
|
||||||
import Styled from './styles';
|
import Styled from './styles';
|
||||||
import { PANELS, ACTIONS } from '../layout/enums';
|
import { PANELS, ACTIONS, LAYOUT_TYPE } from '../layout/enums';
|
||||||
import browserInfo from '/imports/utils/browserInfo';
|
import browserInfo from '/imports/utils/browserInfo';
|
||||||
import Header from '/imports/ui/components/common/control-header/component';
|
import Header from '/imports/ui/components/common/control-header/component';
|
||||||
|
import NotesDropdown from '/imports/ui/components/notes/notes-dropdown/container';
|
||||||
|
|
||||||
|
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||||
|
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
hide: {
|
hide: {
|
||||||
@ -19,6 +22,10 @@ const intlMessages = defineMessages({
|
|||||||
id: 'app.notes.title',
|
id: 'app.notes.title',
|
||||||
description: 'Title for the shared notes',
|
description: 'Title for the shared notes',
|
||||||
},
|
},
|
||||||
|
unpinNotes: {
|
||||||
|
id: 'app.notes.notesDropdown.unpinNotes',
|
||||||
|
description: 'Label for unpin shared notes button',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
@ -29,6 +36,15 @@ const propTypes = {
|
|||||||
hasPermission: PropTypes.bool.isRequired,
|
hasPermission: PropTypes.bool.isRequired,
|
||||||
isResizing: PropTypes.bool.isRequired,
|
isResizing: PropTypes.bool.isRequired,
|
||||||
layoutContextDispatch: PropTypes.func.isRequired,
|
layoutContextDispatch: PropTypes.func.isRequired,
|
||||||
|
sidebarContent: PropTypes.object.isRequired,
|
||||||
|
sharedNotesOutput: PropTypes.object.isRequired,
|
||||||
|
area: PropTypes.string,
|
||||||
|
layoutType: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
area: 'sidebarContent',
|
||||||
|
layoutType: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Notes = ({
|
const Notes = ({
|
||||||
@ -37,12 +53,84 @@ const Notes = ({
|
|||||||
isRTL,
|
isRTL,
|
||||||
layoutContextDispatch,
|
layoutContextDispatch,
|
||||||
isResizing,
|
isResizing,
|
||||||
|
area,
|
||||||
|
layoutType,
|
||||||
|
sidebarContent,
|
||||||
|
sharedNotesOutput,
|
||||||
|
amIPresenter,
|
||||||
}) => {
|
}) => {
|
||||||
useEffect(() => () => Service.setLastRev(), []);
|
useEffect(() => () => Service.setLastRev(), []);
|
||||||
const { isChrome } = browserInfo;
|
const { isChrome } = browserInfo;
|
||||||
|
const isOnMediaArea = area === 'media';
|
||||||
|
const style = isOnMediaArea ? {
|
||||||
|
position: 'absolute',
|
||||||
|
...sharedNotesOutput,
|
||||||
|
} : {};
|
||||||
|
const isHidden = isOnMediaArea && (style.width === 0 || style.height === 0);
|
||||||
|
|
||||||
|
if (isHidden) style.padding = 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
isOnMediaArea
|
||||||
|
&& sidebarContent.isOpen
|
||||||
|
&& sidebarContent.sidebarContentPanel === PANELS.SHARED_NOTES
|
||||||
|
) {
|
||||||
|
if (layoutType === LAYOUT_TYPE.VIDEO_FOCUS) {
|
||||||
|
layoutContextDispatch({
|
||||||
|
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||||
|
value: PANELS.CHAT,
|
||||||
|
});
|
||||||
|
|
||||||
|
layoutContextDispatch({
|
||||||
|
type: ACTIONS.SET_ID_CHAT_OPEN,
|
||||||
|
value: PUBLIC_CHAT_ID,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
layoutContextDispatch({
|
||||||
|
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||||
|
value: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
layoutContextDispatch({
|
||||||
|
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||||
|
value: PANELS.NONE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutContextDispatch({
|
||||||
|
type: ACTIONS.SET_NOTES_IS_PINNED,
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
layoutContextDispatch({
|
||||||
|
type: ACTIONS.SET_NOTES_IS_PINNED,
|
||||||
|
value: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderHeaderOnMedia = () => {
|
||||||
|
return amIPresenter ? (
|
||||||
|
<Styled.Header
|
||||||
|
rightButtonProps={{
|
||||||
|
'aria-label': intl.formatMessage(intlMessages.unpinNotes),
|
||||||
|
'data-test': 'unpinNotes',
|
||||||
|
icon: 'close',
|
||||||
|
label: intl.formatMessage(intlMessages.unpinNotes),
|
||||||
|
onClick: () => {
|
||||||
|
Service.pinSharedNotes(false);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Styled.Notes data-test="notes" isChrome={isChrome}>
|
<Styled.Notes data-test="notes" isChrome={isChrome} style={style}>
|
||||||
|
{!isOnMediaArea ? (
|
||||||
<Header
|
<Header
|
||||||
leftButtonProps={{
|
leftButtonProps={{
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@ -60,9 +148,10 @@ const Notes = ({
|
|||||||
label: intl.formatMessage(intlMessages.title),
|
label: intl.formatMessage(intlMessages.title),
|
||||||
}}
|
}}
|
||||||
customRightButton={
|
customRightButton={
|
||||||
<ConverterButtonContainer />
|
<NotesDropdown />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
) : renderHeaderOnMedia()}
|
||||||
<PadContainer
|
<PadContainer
|
||||||
externalId={Service.ID}
|
externalId={Service.ID}
|
||||||
hasPermission={hasPermission}
|
hasPermission={hasPermission}
|
||||||
@ -74,5 +163,6 @@ const Notes = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
Notes.propTypes = propTypes;
|
Notes.propTypes = propTypes;
|
||||||
|
Notes.defaultProps = defaultProps;
|
||||||
|
|
||||||
export default injectWbResizeEvent(injectIntl(Notes));
|
export default injectWbResizeEvent(injectIntl(Notes));
|
||||||
|
@ -1,15 +1,29 @@
|
|||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { withTracker } from 'meteor/react-meteor-data';
|
import { withTracker } from 'meteor/react-meteor-data';
|
||||||
import Notes from './component';
|
import Notes from './component';
|
||||||
import Service from './service';
|
import Service from './service';
|
||||||
import { layoutSelectInput, layoutDispatch } from '../layout/context';
|
import Auth from '/imports/ui/services/auth';
|
||||||
|
import { layoutSelectInput, layoutDispatch, layoutSelectOutput } from '../layout/context';
|
||||||
|
import { UsersContext } from '../components-data/users-context/context';
|
||||||
|
|
||||||
const Container = ({ ...props }) => {
|
const Container = ({ ...props }) => {
|
||||||
const cameraDock = layoutSelectInput((i) => i.cameraDock);
|
const cameraDock = layoutSelectInput((i) => i.cameraDock);
|
||||||
|
const sharedNotesOutput = layoutSelectOutput((i) => i.sharedNotes);
|
||||||
|
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
|
||||||
const { isResizing } = cameraDock;
|
const { isResizing } = cameraDock;
|
||||||
const layoutContextDispatch = layoutDispatch();
|
const layoutContextDispatch = layoutDispatch();
|
||||||
|
const usingUsersContext = useContext(UsersContext);
|
||||||
|
const { users } = usingUsersContext;
|
||||||
|
const amIPresenter = users[Auth.meetingID][Auth.userID].presenter;
|
||||||
|
|
||||||
return <Notes {...{ layoutContextDispatch, isResizing, ...props }} />;
|
return <Notes {...{
|
||||||
|
layoutContextDispatch,
|
||||||
|
isResizing,
|
||||||
|
sidebarContent,
|
||||||
|
sharedNotesOutput,
|
||||||
|
amIPresenter,
|
||||||
|
...props
|
||||||
|
}} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withTracker(() => {
|
export default withTracker(() => {
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
import Service from './service';
|
|
||||||
import Styled from './styles';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
const DEBOUNCE_TIMEOUT = 15000;
|
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
|
||||||
convertAndUploadLabel: {
|
|
||||||
id: 'app.note.converter-button.convertAndUpload',
|
|
||||||
description: 'Export shared notes as a PDF and upload to the main room',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
intl: PropTypes.shape({
|
|
||||||
formatMessage: PropTypes.func.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
amIPresenter: PropTypes.bool.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConverterButtonComponent = ({
|
|
||||||
intl,
|
|
||||||
amIPresenter,
|
|
||||||
}) => {
|
|
||||||
[converterButtonDisabled, setConverterButtonDisabled] = useState(false);
|
|
||||||
return (amIPresenter
|
|
||||||
? (
|
|
||||||
<Styled.ConvertAndUpload
|
|
||||||
disabled={converterButtonDisabled}
|
|
||||||
onClick={() => {
|
|
||||||
setConverterButtonDisabled(true);
|
|
||||||
setTimeout(() => setConverterButtonDisabled(false), DEBOUNCE_TIMEOUT);
|
|
||||||
return Service.convertAndUpload()}}
|
|
||||||
label={intl.formatMessage(intlMessages.convertAndUploadLabel)}
|
|
||||||
icon="upload"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
: null)};
|
|
||||||
|
|
||||||
ConverterButtonComponent.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default injectIntl(ConverterButtonComponent);
|
|
@ -1,31 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
import {
|
|
||||||
colorWhite,
|
|
||||||
colorGrayDark,
|
|
||||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
|
||||||
import Button from '/imports/ui/components/common/button/component';
|
|
||||||
|
|
||||||
const ConvertAndUpload = styled(Button)`
|
|
||||||
position: relative;
|
|
||||||
background-color: ${colorWhite};
|
|
||||||
display: block;
|
|
||||||
padding: 0;
|
|
||||||
& > i {
|
|
||||||
color: ${colorGrayDark};
|
|
||||||
font-size: smaller;
|
|
||||||
[dir="rtl"] & {
|
|
||||||
-webkit-transform: scale(-1, 1);
|
|
||||||
-moz-transform: scale(-1, 1);
|
|
||||||
-ms-transform: scale(-1, 1);
|
|
||||||
-o-transform: scale(-1, 1);
|
|
||||||
transform: scale(-1, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
background-color: ${colorWhite};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
ConvertAndUpload,
|
|
||||||
};
|
|
@ -0,0 +1,137 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||||
|
import Trigger from '/imports/ui/components/common/control-header/right/component';
|
||||||
|
import Service from './service';
|
||||||
|
|
||||||
|
const DEBOUNCE_TIMEOUT = 15000;
|
||||||
|
const NOTES_CONFIG = Meteor.settings.public.notes;
|
||||||
|
const NOTES_IS_PINNABLE = NOTES_CONFIG.pinnable;
|
||||||
|
|
||||||
|
const intlMessages = defineMessages({
|
||||||
|
convertAndUploadLabel: {
|
||||||
|
id: 'app.notes.notesDropdown.covertAndUpload',
|
||||||
|
description: 'Export shared notes as a PDF and upload to the main room',
|
||||||
|
},
|
||||||
|
pinNotes: {
|
||||||
|
id: 'app.notes.notesDropdown.pinNotes',
|
||||||
|
description: 'Label for pin shared notes button',
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
id: 'app.notes.notesDropdown.notesOptions',
|
||||||
|
description: 'Label for shared notes options',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
intl: PropTypes.shape({
|
||||||
|
formatMessage: PropTypes.func.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
amIPresenter: PropTypes.bool.isRequired,
|
||||||
|
isRTL: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
class NotesDropdown extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
converterButtonDisabled: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setConverterButtonDisabled(value) {
|
||||||
|
return this.setState({ converterButtonDisabled: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailableActions() {
|
||||||
|
const {
|
||||||
|
intl,
|
||||||
|
amIPresenter,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const { converterButtonDisabled } = this.state;
|
||||||
|
|
||||||
|
const uploadIcon = 'upload';
|
||||||
|
const pinIcon = 'presentation';
|
||||||
|
|
||||||
|
this.menuItems = [];
|
||||||
|
|
||||||
|
if (amIPresenter) {
|
||||||
|
this.menuItems.push(
|
||||||
|
{
|
||||||
|
key: _.uniqueId('notes-option-'),
|
||||||
|
icon: uploadIcon,
|
||||||
|
dataTest: 'moveNotesToWhiteboard',
|
||||||
|
label: intl.formatMessage(intlMessages.convertAndUploadLabel),
|
||||||
|
disabled: converterButtonDisabled,
|
||||||
|
onClick: () => {
|
||||||
|
this.setConverterButtonDisabled(true);
|
||||||
|
setTimeout(() => this.setConverterButtonDisabled(false), DEBOUNCE_TIMEOUT);
|
||||||
|
return Service.convertAndUpload();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amIPresenter && NOTES_IS_PINNABLE) {
|
||||||
|
this.menuItems.push(
|
||||||
|
{
|
||||||
|
key: _.uniqueId('notes-option-'),
|
||||||
|
icon: pinIcon,
|
||||||
|
dataTest: 'pinNotes',
|
||||||
|
label: intl.formatMessage(intlMessages.pinNotes),
|
||||||
|
onClick: () => {
|
||||||
|
Service.pinSharedNotes();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.menuItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
intl,
|
||||||
|
isRTL,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const actions = this.getAvailableActions();
|
||||||
|
|
||||||
|
if (actions.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BBBMenu
|
||||||
|
trigger={
|
||||||
|
<Trigger
|
||||||
|
data-test="notesOptionsMenu"
|
||||||
|
icon="more"
|
||||||
|
label={intl.formatMessage(intlMessages.options)}
|
||||||
|
aria-label={intl.formatMessage(intlMessages.options)}
|
||||||
|
onClick={() => null}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
opts={{
|
||||||
|
id: 'notes-options-dropdown',
|
||||||
|
keepMounted: true,
|
||||||
|
transitionDuration: 0,
|
||||||
|
elevation: 3,
|
||||||
|
getContentAnchorEl: null,
|
||||||
|
fullwidth: 'true',
|
||||||
|
anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
|
||||||
|
transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
|
||||||
|
}}
|
||||||
|
actions={actions}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotesDropdown.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default injectIntl(NotesDropdown);
|
@ -1,14 +1,16 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import ConverterButton from './component';
|
import NotesDropdown from './component';
|
||||||
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
|
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
|
||||||
import Auth from '/imports/ui/services/auth';
|
import Auth from '/imports/ui/services/auth';
|
||||||
|
import { layoutSelect } from '/imports/ui/components/layout/context';
|
||||||
|
|
||||||
const ConverterButtonContainer = ({ ...props }) => {
|
const NotesDropdownContainer = ({ ...props }) => {
|
||||||
const usingUsersContext = useContext(UsersContext);
|
const usingUsersContext = useContext(UsersContext);
|
||||||
const { users } = usingUsersContext;
|
const { users } = usingUsersContext;
|
||||||
const amIPresenter = users[Auth.meetingID][Auth.userID].presenter;
|
const amIPresenter = users[Auth.meetingID][Auth.userID].presenter;
|
||||||
|
const isRTL = layoutSelect((i) => i.isRTL);
|
||||||
|
|
||||||
return <ConverterButton {...{ amIPresenter, ...props }} />;
|
return <NotesDropdown {...{ amIPresenter, isRTL, ...props }} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ConverterButtonContainer;
|
export default NotesDropdownContainer;
|
@ -6,7 +6,6 @@ import { UploadingPresentations } from '/imports/api/presentations';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
const PADS_CONFIG = Meteor.settings.public.pads;
|
const PADS_CONFIG = Meteor.settings.public.pads;
|
||||||
const PRESENTATION_CONFIG = Meteor.settings.public.presentation;
|
|
||||||
|
|
||||||
async function convertAndUpload() {
|
async function convertAndUpload() {
|
||||||
|
|
||||||
@ -30,8 +29,8 @@ async function convertAndUpload() {
|
|||||||
done: false,
|
done: false,
|
||||||
error: false
|
error: false
|
||||||
},
|
},
|
||||||
uploadTimestamp: new Date()
|
uploadTimestamp: new Date(),
|
||||||
})
|
});
|
||||||
|
|
||||||
const exportUrl = Auth.authenticateURL(`${PADS_CONFIG.url}/p/${padId}/export/${extension}?${params}`);
|
const exportUrl = Auth.authenticateURL(`${PADS_CONFIG.url}/p/${padId}/export/${extension}?${params}`);
|
||||||
const sharedNotesAsFile = await fetch(exportUrl, { credentials: 'include' });
|
const sharedNotesAsFile = await fetch(exportUrl, { credentials: 'include' });
|
||||||
@ -51,13 +50,14 @@ async function convertAndUpload() {
|
|||||||
conversion: { done: false, error: false },
|
conversion: { done: false, error: false },
|
||||||
upload: { done: false, error: false, progress: 0 },
|
upload: { done: false, error: false, progress: 0 },
|
||||||
exportation: { isRunning: false, error: false },
|
exportation: { isRunning: false, error: false },
|
||||||
onConversion: () => {},
|
onConversion: () => { },
|
||||||
onUpload: () => {},
|
onUpload: () => { },
|
||||||
onProgress: () => {},
|
onProgress: () => { },
|
||||||
onDone: () => {},
|
onDone: () => { },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
convertAndUpload,
|
convertAndUpload,
|
||||||
|
pinSharedNotes: () => NotesService.pinSharedNotes(true),
|
||||||
};
|
};
|
@ -73,6 +73,15 @@ const toggleNotesPanel = (sidebarContentPanel, layoutContextDispatch) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pinSharedNotes = (pinned) => {
|
||||||
|
PadsService.pinPad(NOTES_CONFIG.id, pinned);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSharedNotesPinned = () => {
|
||||||
|
const pinnedPad = PadsService.getPinnedPad();
|
||||||
|
return pinnedPad?.externalId === NOTES_CONFIG.id;
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
ID: NOTES_CONFIG.id,
|
ID: NOTES_CONFIG.id,
|
||||||
toggleNotesPanel,
|
toggleNotesPanel,
|
||||||
@ -81,4 +90,6 @@ export default {
|
|||||||
setLastRev,
|
setLastRev,
|
||||||
getLastRev,
|
getLastRev,
|
||||||
hasUnreadNotes,
|
hasUnreadNotes,
|
||||||
|
isSharedNotesPinned,
|
||||||
|
pinSharedNotes,
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
} from '/imports/ui/stylesheets/styled-components/general';
|
} from '/imports/ui/stylesheets/styled-components/general';
|
||||||
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
|
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
|
||||||
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
|
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
|
||||||
|
import CommonHeader from '/imports/ui/components/common/control-header/component';
|
||||||
|
|
||||||
const Notes = styled.div`
|
const Notes = styled.div`
|
||||||
background-color: ${colorWhite};
|
background-color: ${colorWhite};
|
||||||
@ -23,4 +24,11 @@ const Notes = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default { Notes };
|
const Header = styled(CommonHeader)`
|
||||||
|
padding-bottom: .2rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Notes,
|
||||||
|
Header,
|
||||||
|
};
|
||||||
|
@ -3,6 +3,14 @@ import Pads, { PadsSessions, PadsUpdates } from '/imports/api/pads';
|
|||||||
import { makeCall } from '/imports/ui/services/api';
|
import { makeCall } from '/imports/ui/services/api';
|
||||||
import Auth from '/imports/ui/services/auth';
|
import Auth from '/imports/ui/services/auth';
|
||||||
import Settings from '/imports/ui/services/settings';
|
import Settings from '/imports/ui/services/settings';
|
||||||
|
import {
|
||||||
|
getVideoUrl,
|
||||||
|
stopWatching,
|
||||||
|
} from '/imports/ui/components/external-video-player/service';
|
||||||
|
import {
|
||||||
|
screenshareHasEnded,
|
||||||
|
isVideoBroadcasting,
|
||||||
|
} from '/imports/ui/components/screenshare/service';
|
||||||
|
|
||||||
const PADS_CONFIG = Meteor.settings.public.pads;
|
const PADS_CONFIG = Meteor.settings.public.pads;
|
||||||
const THROTTLE_TIMEOUT = 2000;
|
const THROTTLE_TIMEOUT = 2000;
|
||||||
@ -96,6 +104,36 @@ const getPadContent = (externalId) => {
|
|||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPinnedPad = () => {
|
||||||
|
const pad = Pads.findOne({
|
||||||
|
meetingId: Auth.meetingID,
|
||||||
|
pinned: true,
|
||||||
|
}, {
|
||||||
|
fields: {
|
||||||
|
externalId: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return pad;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pinPad = (externalId, pinned) => {
|
||||||
|
if (pinned) {
|
||||||
|
// Stop external video sharing if it's running.
|
||||||
|
if (getVideoUrl()) stopWatching();
|
||||||
|
|
||||||
|
// Stop screen sharing if it's running.
|
||||||
|
if (isVideoBroadcasting()) screenshareHasEnded();
|
||||||
|
}
|
||||||
|
|
||||||
|
makeCall('pinPad', externalId, pinned);
|
||||||
|
};
|
||||||
|
|
||||||
|
const throttledPinPad = _.throttle(pinPad, 1000, {
|
||||||
|
leading: true,
|
||||||
|
trailing: false,
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getPadId,
|
getPadId,
|
||||||
createGroup,
|
createGroup,
|
||||||
@ -106,4 +144,6 @@ export default {
|
|||||||
getPadTail,
|
getPadTail,
|
||||||
getPadContent,
|
getPadContent,
|
||||||
getParams,
|
getParams,
|
||||||
|
getPinnedPad,
|
||||||
|
pinPad: throttledPinPad,
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,7 @@ import { Meteor } from "meteor/meteor";
|
|||||||
import MediaStreamUtils from '/imports/utils/media-stream-utils';
|
import MediaStreamUtils from '/imports/utils/media-stream-utils';
|
||||||
import ConnectionStatusService from '/imports/ui/components/connection-status/service';
|
import ConnectionStatusService from '/imports/ui/components/connection-status/service';
|
||||||
import browserInfo from '/imports/utils/browserInfo';
|
import browserInfo from '/imports/utils/browserInfo';
|
||||||
|
import NotesService from '/imports/ui/components/notes/service';
|
||||||
|
|
||||||
const VOLUME_CONTROL_ENABLED = Meteor.settings.public.kurento.screenshare.enableVolumeControl;
|
const VOLUME_CONTROL_ENABLED = Meteor.settings.public.kurento.screenshare.enableVolumeControl;
|
||||||
const SCREENSHARE_MEDIA_ELEMENT_NAME = 'screenshareVideo';
|
const SCREENSHARE_MEDIA_ELEMENT_NAME = 'screenshareVideo';
|
||||||
@ -176,6 +177,9 @@ const shareScreen = async (isPresenter, onFail) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close Shared Notes if open.
|
||||||
|
NotesService.pinSharedNotes(false);
|
||||||
|
|
||||||
setSharingScreen(true);
|
setSharingScreen(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onFail(error);
|
onFail(error);
|
||||||
|
@ -34,6 +34,10 @@ const intlMessages = defineMessages({
|
|||||||
id: 'app.userList.byModerator',
|
id: 'app.userList.byModerator',
|
||||||
description: '',
|
description: '',
|
||||||
},
|
},
|
||||||
|
disabled: {
|
||||||
|
id: 'app.notes.disabled',
|
||||||
|
description: 'Aria description for disabled notes button',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
class UserNotes extends Component {
|
class UserNotes extends Component {
|
||||||
@ -57,7 +61,7 @@ class UserNotes extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { sidebarContentPanel, rev } = this.props;
|
const { sidebarContentPanel, rev, isPinned } = this.props;
|
||||||
const { unread } = this.state;
|
const { unread } = this.state;
|
||||||
|
|
||||||
if (sidebarContentPanel !== PANELS.SHARED_NOTES && !unread) {
|
if (sidebarContentPanel !== PANELS.SHARED_NOTES && !unread) {
|
||||||
@ -67,6 +71,8 @@ class UserNotes extends Component {
|
|||||||
if (sidebarContentPanel === PANELS.SHARED_NOTES && unread) {
|
if (sidebarContentPanel === PANELS.SHARED_NOTES && unread) {
|
||||||
this.setUnread(false);
|
this.setUnread(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isPinned && prevProps.isPinned && unread) this.setUnread(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setUnread(unread) {
|
setUnread(unread) {
|
||||||
@ -79,11 +85,12 @@ class UserNotes extends Component {
|
|||||||
disableNotes,
|
disableNotes,
|
||||||
sidebarContentPanel,
|
sidebarContentPanel,
|
||||||
layoutContextDispatch,
|
layoutContextDispatch,
|
||||||
|
isPinned,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { unread } = this.state;
|
const { unread } = this.state;
|
||||||
|
|
||||||
let notification = null;
|
let notification = null;
|
||||||
if (unread) {
|
if (unread && !isPinned) {
|
||||||
notification = (
|
notification = (
|
||||||
<Styled.UnreadMessages aria-label={intl.formatMessage(intlMessages.unreadContent)}>
|
<Styled.UnreadMessages aria-label={intl.formatMessage(intlMessages.unreadContent)}>
|
||||||
<Styled.UnreadMessagesText aria-hidden="true">
|
<Styled.UnreadMessagesText aria-hidden="true">
|
||||||
@ -101,6 +108,9 @@ class UserNotes extends Component {
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => NotesService.toggleNotesPanel(sidebarContentPanel, layoutContextDispatch)}
|
onClick={() => NotesService.toggleNotesPanel(sidebarContentPanel, layoutContextDispatch)}
|
||||||
onKeyPress={() => { }}
|
onKeyPress={() => { }}
|
||||||
|
as={isPinned ? 'button' : 'div'}
|
||||||
|
disabled={isPinned}
|
||||||
|
$disabled={isPinned}
|
||||||
>
|
>
|
||||||
<Icon iconName="copy" />
|
<Icon iconName="copy" />
|
||||||
<div aria-hidden>
|
<div aria-hidden>
|
||||||
@ -114,6 +124,10 @@ class UserNotes extends Component {
|
|||||||
<span id="lockedNotes">{`${intl.formatMessage(intlMessages.locked)} ${intl.formatMessage(intlMessages.byModerator)}`}</span>
|
<span id="lockedNotes">{`${intl.formatMessage(intlMessages.locked)} ${intl.formatMessage(intlMessages.byModerator)}`}</span>
|
||||||
</Styled.NotesLock>
|
</Styled.NotesLock>
|
||||||
) : null}
|
) : null}
|
||||||
|
{isPinned
|
||||||
|
? (
|
||||||
|
<span className='sr-only'>{`${intl.formatMessage(intlMessages.disabled)}`}</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{notification}
|
{notification}
|
||||||
</Styled.ListItem>
|
</Styled.ListItem>
|
||||||
|
@ -18,5 +18,6 @@ export default lockContextContainer(withTracker(({ userLocks }) => {
|
|||||||
return {
|
return {
|
||||||
rev: PadsService.getRev(NotesService.ID),
|
rev: PadsService.getRev(NotesService.ID),
|
||||||
disableNotes: shouldDisableNotes,
|
disableNotes: shouldDisableNotes,
|
||||||
|
isPinned: NotesService.isSharedNotesPinned(),
|
||||||
};
|
};
|
||||||
})(UserNotesContainer));
|
})(UserNotesContainer));
|
||||||
|
@ -13,7 +13,12 @@ const UnreadMessages = styled(StyledContent.UnreadMessages)``;
|
|||||||
|
|
||||||
const UnreadMessagesText = styled(StyledContent.UnreadMessagesText)``;
|
const UnreadMessagesText = styled(StyledContent.UnreadMessagesText)``;
|
||||||
|
|
||||||
const ListItem = styled(StyledContent.ListItem)``;
|
const ListItem = styled(StyledContent.ListItem)`
|
||||||
|
${({ $disabled }) => $disabled && `
|
||||||
|
cursor: not-allowed;
|
||||||
|
border: none;
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
const NotesTitle = styled.div`
|
const NotesTitle = styled.div`
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
@ -528,6 +528,7 @@ public:
|
|||||||
notes:
|
notes:
|
||||||
enabled: true
|
enabled: true
|
||||||
id: notes
|
id: notes
|
||||||
|
pinnable: true
|
||||||
layout:
|
layout:
|
||||||
hidePresentation: false
|
hidePresentation: false
|
||||||
showParticipantsOnLogin: true
|
showParticipantsOnLogin: true
|
||||||
|
@ -84,8 +84,12 @@
|
|||||||
"app.notes.title": "Shared Notes",
|
"app.notes.title": "Shared Notes",
|
||||||
"app.notes.label": "Notes",
|
"app.notes.label": "Notes",
|
||||||
"app.notes.hide": "Hide notes",
|
"app.notes.hide": "Hide notes",
|
||||||
"app.note.converter-button.convertAndUpload": "Move notes to whiteboard",
|
|
||||||
"app.notes.locked": "Locked",
|
"app.notes.locked": "Locked",
|
||||||
|
"app.notes.disabled": "Pinned on media area",
|
||||||
|
"app.notes.notesDropdown.covertAndUpload": "Convert notes to presentation",
|
||||||
|
"app.notes.notesDropdown.pinNotes": "Pin notes onto whiteboard",
|
||||||
|
"app.notes.notesDropdown.unpinNotes": "Unpin notes",
|
||||||
|
"app.notes.notesDropdown.notesOptions": "Notes options",
|
||||||
"app.pads.hint": "Press Esc to focus the pad's toolbar",
|
"app.pads.hint": "Press Esc to focus the pad's toolbar",
|
||||||
"app.user.activityCheck": "User activity check",
|
"app.user.activityCheck": "User activity check",
|
||||||
"app.user.activityCheck.label": "Check if user is still in meeting ({0})",
|
"app.user.activityCheck.label": "Check if user is still in meeting ({0})",
|
||||||
|
Loading…
Reference in New Issue
Block a user