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 PadContentSysMsgHdlr
|
||||
with PadPatchSysMsgHdlr
|
||||
with PadUpdatePubMsgHdlr {
|
||||
with PadUpdatePubMsgHdlr
|
||||
with PadPinnedReqMsgHdlr {
|
||||
|
||||
}
|
||||
|
@ -175,6 +175,8 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeGenericMsg[PadUpdatePubMsg](envelope, jsonNode)
|
||||
case PadCapturePubMsg.NAME =>
|
||||
routePadMsg[PadCapturePubMsg](envelope, jsonNode)
|
||||
case PadPinnedReqMsg.NAME =>
|
||||
routeGenericMsg[PadPinnedReqMsg](envelope, jsonNode)
|
||||
|
||||
// Voice
|
||||
case RecordingStartedVoiceConfEvtMsg.NAME =>
|
||||
|
@ -492,6 +492,7 @@ class MeetingActor(
|
||||
case m: PadContentSysMsg => padsApp2x.handle(m, liveMeeting, msgBus)
|
||||
case m: PadPatchSysMsg => padsApp2x.handle(m, liveMeeting, msgBus)
|
||||
case m: PadUpdatePubMsg => padsApp2x.handle(m, liveMeeting, msgBus)
|
||||
case m: PadPinnedReqMsg => padsApp2x.handle(m, liveMeeting, msgBus)
|
||||
|
||||
// Lock Settings
|
||||
case m: ChangeLockSettingsInMeetingCmdMsg =>
|
||||
|
@ -118,3 +118,13 @@ case class PadUpdateCmdMsgBody(groupId: String, name: String, text: String)
|
||||
object PadCapturePubMsg { val NAME = "PadCapturePubMsg" }
|
||||
case class PadCapturePubMsg(header: BbbCoreHeaderWithMeetingId, body: PadCapturePubMsgBody) extends PadStandardMsg
|
||||
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 sessionDeleted from './handlers/sessionDeleted';
|
||||
import captureSharedNotes from './handlers/captureSharedNotes';
|
||||
import padPinned from './handlers/padPinned';
|
||||
|
||||
RedisPubSub.on('PadGroupCreatedRespMsg', groupCreated);
|
||||
RedisPubSub.on('PadCreatedRespMsg', padCreated);
|
||||
@ -16,3 +17,4 @@ RedisPubSub.on('PadContentEvtMsg', padContent);
|
||||
RedisPubSub.on('PadTailEvtMsg', padTail);
|
||||
RedisPubSub.on('PadSessionDeletedEvtMsg', sessionDeleted);
|
||||
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 createSession from './methods/createSession';
|
||||
import getPadId from './methods/getPadId';
|
||||
import pinPad from './methods/pinPad';
|
||||
|
||||
Meteor.methods({
|
||||
createGroup,
|
||||
createSession,
|
||||
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 = {
|
||||
$set: {
|
||||
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 ActionsBarContainer from '../actions-bar/container';
|
||||
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 APP_CONFIG = Meteor.settings.public.app;
|
||||
@ -479,6 +480,7 @@ class App extends Component {
|
||||
shouldShowPresentation,
|
||||
shouldShowScreenshare,
|
||||
shouldShowExternalVideo,
|
||||
shouldShowSharedNotes,
|
||||
isPresenter,
|
||||
selectedLayout,
|
||||
presentationIsOpen,
|
||||
@ -513,6 +515,7 @@ class App extends Component {
|
||||
? <ExternalVideoContainer isLayoutSwapped={!presentationIsOpen} isPresenter={isPresenter} />
|
||||
: null
|
||||
}
|
||||
{shouldShowSharedNotes ? <NotesContainer area="media" layoutType={selectedLayout} /> : null}
|
||||
{this.renderCaptions()}
|
||||
<AudioCaptionsSpeechContainer />
|
||||
{this.renderAudioCaptions()}
|
||||
|
@ -254,6 +254,7 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
|
||||
const AppSettings = Settings.application;
|
||||
const { selectedLayout, pushLayout } = AppSettings;
|
||||
const { viewScreenshare } = Settings.dataSaving;
|
||||
const shouldShowSharedNotes = MediaService.shouldShowSharedNotes();
|
||||
const shouldShowExternalVideo = MediaService.shouldShowExternalVideo();
|
||||
const shouldShowScreenshare = MediaService.shouldShowScreenshare()
|
||||
&& (viewScreenshare || currentUser?.presenter);
|
||||
@ -298,8 +299,9 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
|
||||
pushAlertEnabled: AppSettings.chatPushAlerts,
|
||||
darkTheme: AppSettings.darkTheme,
|
||||
shouldShowScreenshare,
|
||||
shouldShowPresentation: !shouldShowScreenshare && !shouldShowExternalVideo,
|
||||
shouldShowPresentation: !shouldShowScreenshare && !shouldShowExternalVideo && !shouldShowSharedNotes,
|
||||
shouldShowExternalVideo,
|
||||
shouldShowSharedNotes,
|
||||
isLargeFont: Session.get('isLargeFont'),
|
||||
presentationRestoreOnUpdate: getFromUserSettings(
|
||||
'bbb_force_restore_presentation_on_new_events',
|
||||
|
@ -9,6 +9,7 @@ const Header = ({
|
||||
rightButtonProps,
|
||||
customRightButton,
|
||||
'data-test': dataTest,
|
||||
...rest
|
||||
}) => {
|
||||
const renderCloseButton = () => (
|
||||
<Right {...rightButtonProps} />
|
||||
@ -21,8 +22,8 @@ const Header = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<Styled.Header data-test={dataTest ? dataTest : ''}>
|
||||
<Left {...leftButtonProps} />
|
||||
<Styled.Header data-test={dataTest ? dataTest : ''} {...rest}>
|
||||
{leftButtonProps ? <Left {...leftButtonProps} /> : <div />}
|
||||
{customRightButton
|
||||
? renderCustomRightButton()
|
||||
: rightButtonProps
|
||||
|
@ -3,6 +3,7 @@ import Auth from '/imports/ui/services/auth';
|
||||
|
||||
import { getStreamer } from '/imports/api/external-videos';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import NotesService from '/imports/ui/components/notes/service';
|
||||
|
||||
import ReactPlayer from 'react-player';
|
||||
|
||||
@ -29,6 +30,10 @@ const startWatching = (url) => {
|
||||
} else if (Panopto.canPlay(url)) {
|
||||
externalVideoUrl = Panopto.getSocialUrl(url);
|
||||
}
|
||||
|
||||
// Close Shared Notes if open.
|
||||
NotesService.pinSharedNotes(false);
|
||||
|
||||
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: {
|
||||
throw new Error('Unexpected action');
|
||||
}
|
||||
|
@ -94,6 +94,9 @@ export const ACTIONS = {
|
||||
SET_HAS_EXTERNAL_VIDEO: 'setHasExternalVideo',
|
||||
SET_EXTERNAL_VIDEO_SIZE: 'setExternalVideoSize',
|
||||
SET_EXTERNAL_VIDEO_OUTPUT: 'setExternalVideoOutput',
|
||||
|
||||
SET_SHARED_NOTES_OUTPUT: 'setSharedNotesOutput',
|
||||
SET_NOTES_IS_PINNED: 'setNotesIsPinned',
|
||||
};
|
||||
|
||||
export const PANELS = {
|
||||
|
@ -95,6 +95,13 @@ export const INITIAL_INPUT_STATE = {
|
||||
browserWidth: 0,
|
||||
browserHeight: 0,
|
||||
},
|
||||
sharedNotes: {
|
||||
isPinned: false,
|
||||
width: 0,
|
||||
height: 0,
|
||||
browserWidth: 0,
|
||||
browserHeight: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const INITIAL_OUTPUT_STATE = {
|
||||
@ -227,4 +234,13 @@ export const INITIAL_OUTPUT_STATE = {
|
||||
tabOrder: 0,
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
|
@ -470,6 +470,17 @@ const PresentationFocusLayout = (props) => {
|
||||
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;
|
||||
|
@ -536,6 +536,17 @@ const SmartLayout = (props) => {
|
||||
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;
|
||||
|
@ -491,6 +491,17 @@ const VideoFocusLayout = (props) => {
|
||||
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;
|
||||
|
@ -6,6 +6,7 @@ import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import { isExternalVideoEnabled, isScreenSharingEnabled } from '/imports/ui/services/features';
|
||||
import { ACTIONS } from '../layout/enums';
|
||||
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 KURENTO_CONFIG = Meteor.settings.public.kurento;
|
||||
@ -34,6 +35,10 @@ function shouldShowExternalVideo() {
|
||||
return isExternalVideoEnabled() && getVideoUrl();
|
||||
}
|
||||
|
||||
function shouldShowSharedNotes() {
|
||||
return NotesService.isSharedNotesPinned();
|
||||
}
|
||||
|
||||
function shouldShowOverlay() {
|
||||
return getFromUserSettings('bbb_enable_video', KURENTO_CONFIG.enableVideo);
|
||||
}
|
||||
@ -53,4 +58,5 @@ export default {
|
||||
shouldShowOverlay,
|
||||
isVideoBroadcasting,
|
||||
setPresentationIsOpen,
|
||||
shouldShowSharedNotes,
|
||||
};
|
||||
|
@ -42,11 +42,13 @@ const NavBarContainer = ({ children, ...props }) => {
|
||||
const sidebarNavigation = layoutSelectInput((i) => i.sidebarNavigation);
|
||||
const navBar = layoutSelectOutput((i) => i.navBar);
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
const sharedNotes = layoutSelectInput((i) => i.sharedNotes);
|
||||
const { isPinned: notesIsPinned } = sharedNotes;
|
||||
|
||||
const { sidebarContentPanel } = sidebarContent;
|
||||
const { sidebarNavPanel } = sidebarNavigation;
|
||||
|
||||
const hasUnreadNotes = sidebarContentPanel !== PANELS.SHARED_NOTES && unread;
|
||||
const hasUnreadNotes = sidebarContentPanel !== PANELS.SHARED_NOTES && unread && !notesIsPinned;
|
||||
const hasUnreadMessages = checkUnreadMessages(
|
||||
{ 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 Service from '/imports/ui/components/notes/service';
|
||||
import PadContainer from '/imports/ui/components/pads/container';
|
||||
import ConverterButtonContainer from './converter-button/container';
|
||||
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 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({
|
||||
hide: {
|
||||
@ -19,6 +22,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.notes.title',
|
||||
description: 'Title for the shared notes',
|
||||
},
|
||||
unpinNotes: {
|
||||
id: 'app.notes.notesDropdown.unpinNotes',
|
||||
description: 'Label for unpin shared notes button',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
@ -29,6 +36,15 @@ const propTypes = {
|
||||
hasPermission: PropTypes.bool.isRequired,
|
||||
isResizing: PropTypes.bool.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 = ({
|
||||
@ -37,32 +53,105 @@ const Notes = ({
|
||||
isRTL,
|
||||
layoutContextDispatch,
|
||||
isResizing,
|
||||
area,
|
||||
layoutType,
|
||||
sidebarContent,
|
||||
sharedNotesOutput,
|
||||
amIPresenter,
|
||||
}) => {
|
||||
useEffect(() => () => Service.setLastRev(), []);
|
||||
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 (
|
||||
<Styled.Notes data-test="notes" isChrome={isChrome}>
|
||||
<Header
|
||||
leftButtonProps={{
|
||||
onClick: () => {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.NONE,
|
||||
});
|
||||
},
|
||||
'data-test': 'hideNotesLabel',
|
||||
'aria-label': intl.formatMessage(intlMessages.hide),
|
||||
label: intl.formatMessage(intlMessages.title),
|
||||
}}
|
||||
customRightButton={
|
||||
<ConverterButtonContainer />
|
||||
}
|
||||
/>
|
||||
<Styled.Notes data-test="notes" isChrome={isChrome} style={style}>
|
||||
{!isOnMediaArea ? (
|
||||
<Header
|
||||
leftButtonProps={{
|
||||
onClick: () => {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: false,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.NONE,
|
||||
});
|
||||
},
|
||||
'data-test': 'hideNotesLabel',
|
||||
'aria-label': intl.formatMessage(intlMessages.hide),
|
||||
label: intl.formatMessage(intlMessages.title),
|
||||
}}
|
||||
customRightButton={
|
||||
<NotesDropdown />
|
||||
}
|
||||
/>
|
||||
) : renderHeaderOnMedia()}
|
||||
<PadContainer
|
||||
externalId={Service.ID}
|
||||
hasPermission={hasPermission}
|
||||
@ -74,5 +163,6 @@ const Notes = ({
|
||||
};
|
||||
|
||||
Notes.propTypes = propTypes;
|
||||
Notes.defaultProps = defaultProps;
|
||||
|
||||
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 Notes from './component';
|
||||
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 cameraDock = layoutSelectInput((i) => i.cameraDock);
|
||||
const sharedNotesOutput = layoutSelectOutput((i) => i.sharedNotes);
|
||||
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
|
||||
const { isResizing } = cameraDock;
|
||||
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(() => {
|
||||
|
@ -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 ConverterButton from './component';
|
||||
import NotesDropdown from './component';
|
||||
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
|
||||
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 { users } = usingUsersContext;
|
||||
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';
|
||||
|
||||
const PADS_CONFIG = Meteor.settings.public.pads;
|
||||
const PRESENTATION_CONFIG = Meteor.settings.public.presentation;
|
||||
|
||||
async function convertAndUpload() {
|
||||
|
||||
@ -30,8 +29,8 @@ async function convertAndUpload() {
|
||||
done: false,
|
||||
error: false
|
||||
},
|
||||
uploadTimestamp: new Date()
|
||||
})
|
||||
uploadTimestamp: new Date(),
|
||||
});
|
||||
|
||||
const exportUrl = Auth.authenticateURL(`${PADS_CONFIG.url}/p/${padId}/export/${extension}?${params}`);
|
||||
const sharedNotesAsFile = await fetch(exportUrl, { credentials: 'include' });
|
||||
@ -51,13 +50,14 @@ async function convertAndUpload() {
|
||||
conversion: { done: false, error: false },
|
||||
upload: { done: false, error: false, progress: 0 },
|
||||
exportation: { isRunning: false, error: false },
|
||||
onConversion: () => {},
|
||||
onUpload: () => {},
|
||||
onProgress: () => {},
|
||||
onDone: () => {},
|
||||
})
|
||||
onConversion: () => { },
|
||||
onUpload: () => { },
|
||||
onProgress: () => { },
|
||||
onDone: () => { },
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
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 {
|
||||
ID: NOTES_CONFIG.id,
|
||||
toggleNotesPanel,
|
||||
@ -81,4 +90,6 @@ export default {
|
||||
setLastRev,
|
||||
getLastRev,
|
||||
hasUnreadNotes,
|
||||
isSharedNotesPinned,
|
||||
pinSharedNotes,
|
||||
};
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
} 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';
|
||||
import CommonHeader from '/imports/ui/components/common/control-header/component';
|
||||
|
||||
const Notes = styled.div`
|
||||
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 Auth from '/imports/ui/services/auth';
|
||||
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 THROTTLE_TIMEOUT = 2000;
|
||||
@ -96,6 +104,36 @@ const getPadContent = (externalId) => {
|
||||
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 {
|
||||
getPadId,
|
||||
createGroup,
|
||||
@ -106,4 +144,6 @@ export default {
|
||||
getPadTail,
|
||||
getPadContent,
|
||||
getParams,
|
||||
getPinnedPad,
|
||||
pinPad: throttledPinPad,
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ import { Meteor } from "meteor/meteor";
|
||||
import MediaStreamUtils from '/imports/utils/media-stream-utils';
|
||||
import ConnectionStatusService from '/imports/ui/components/connection-status/service';
|
||||
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 SCREENSHARE_MEDIA_ELEMENT_NAME = 'screenshareVideo';
|
||||
@ -176,6 +177,9 @@ const shareScreen = async (isPresenter, onFail) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close Shared Notes if open.
|
||||
NotesService.pinSharedNotes(false);
|
||||
|
||||
setSharingScreen(true);
|
||||
} catch (error) {
|
||||
onFail(error);
|
||||
|
@ -34,6 +34,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.userList.byModerator',
|
||||
description: '',
|
||||
},
|
||||
disabled: {
|
||||
id: 'app.notes.disabled',
|
||||
description: 'Aria description for disabled notes button',
|
||||
},
|
||||
});
|
||||
|
||||
class UserNotes extends Component {
|
||||
@ -57,7 +61,7 @@ class UserNotes extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { sidebarContentPanel, rev } = this.props;
|
||||
const { sidebarContentPanel, rev, isPinned } = this.props;
|
||||
const { unread } = this.state;
|
||||
|
||||
if (sidebarContentPanel !== PANELS.SHARED_NOTES && !unread) {
|
||||
@ -67,6 +71,8 @@ class UserNotes extends Component {
|
||||
if (sidebarContentPanel === PANELS.SHARED_NOTES && unread) {
|
||||
this.setUnread(false);
|
||||
}
|
||||
|
||||
if (!isPinned && prevProps.isPinned && unread) this.setUnread(false);
|
||||
}
|
||||
|
||||
setUnread(unread) {
|
||||
@ -79,11 +85,12 @@ class UserNotes extends Component {
|
||||
disableNotes,
|
||||
sidebarContentPanel,
|
||||
layoutContextDispatch,
|
||||
isPinned,
|
||||
} = this.props;
|
||||
const { unread } = this.state;
|
||||
|
||||
let notification = null;
|
||||
if (unread) {
|
||||
if (unread && !isPinned) {
|
||||
notification = (
|
||||
<Styled.UnreadMessages aria-label={intl.formatMessage(intlMessages.unreadContent)}>
|
||||
<Styled.UnreadMessagesText aria-hidden="true">
|
||||
@ -101,6 +108,9 @@ class UserNotes extends Component {
|
||||
tabIndex={0}
|
||||
onClick={() => NotesService.toggleNotesPanel(sidebarContentPanel, layoutContextDispatch)}
|
||||
onKeyPress={() => { }}
|
||||
as={isPinned ? 'button' : 'div'}
|
||||
disabled={isPinned}
|
||||
$disabled={isPinned}
|
||||
>
|
||||
<Icon iconName="copy" />
|
||||
<div aria-hidden>
|
||||
@ -114,6 +124,10 @@ class UserNotes extends Component {
|
||||
<span id="lockedNotes">{`${intl.formatMessage(intlMessages.locked)} ${intl.formatMessage(intlMessages.byModerator)}`}</span>
|
||||
</Styled.NotesLock>
|
||||
) : null}
|
||||
{isPinned
|
||||
? (
|
||||
<span className='sr-only'>{`${intl.formatMessage(intlMessages.disabled)}`}</span>
|
||||
) : null}
|
||||
</div>
|
||||
{notification}
|
||||
</Styled.ListItem>
|
||||
|
@ -18,5 +18,6 @@ export default lockContextContainer(withTracker(({ userLocks }) => {
|
||||
return {
|
||||
rev: PadsService.getRev(NotesService.ID),
|
||||
disableNotes: shouldDisableNotes,
|
||||
isPinned: NotesService.isSharedNotesPinned(),
|
||||
};
|
||||
})(UserNotesContainer));
|
||||
|
@ -13,7 +13,12 @@ const UnreadMessages = styled(StyledContent.UnreadMessages)``;
|
||||
|
||||
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`
|
||||
font-weight: 400;
|
||||
|
@ -528,6 +528,7 @@ public:
|
||||
notes:
|
||||
enabled: true
|
||||
id: notes
|
||||
pinnable: true
|
||||
layout:
|
||||
hidePresentation: false
|
||||
showParticipantsOnLogin: true
|
||||
|
@ -84,8 +84,12 @@
|
||||
"app.notes.title": "Shared Notes",
|
||||
"app.notes.label": "Notes",
|
||||
"app.notes.hide": "Hide notes",
|
||||
"app.note.converter-button.convertAndUpload": "Move notes to whiteboard",
|
||||
"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.user.activityCheck": "User activity check",
|
||||
"app.user.activityCheck.label": "Check if user is still in meeting ({0})",
|
||||
|
Loading…
Reference in New Issue
Block a user