Merge pull request #19448 from JoVictorNunes/migrate-pads
refactor(notes / captions): migrate pads from Meteor to GraphQL
This commit is contained in:
commit
4885b4b5d8
@ -1,12 +1,6 @@
|
||||
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,
|
||||
});
|
||||
|
@ -1,27 +0,0 @@
|
||||
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 createSession(externalId) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'PadCreateSessionReqMsg';
|
||||
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(externalId, String);
|
||||
|
||||
const payload = {
|
||||
externalId,
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method createSession ${err.stack}`);
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Pads from '/imports/api/pads';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
|
||||
export default async function getPadId(externalId) {
|
||||
try {
|
||||
const { meetingId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(externalId, String);
|
||||
|
||||
const pad = await Pads.findOneAsync(
|
||||
{
|
||||
meetingId,
|
||||
externalId,
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
padId: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (pad && pad.padId) {
|
||||
return pad.padId;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
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}`);
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ import UserInfoContainer from '/imports/ui/components/user-info/container';
|
||||
import BreakoutRoomInvitation from '/imports/ui/components/breakout-room/invitation/container';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import ToastContainer from '/imports/ui/components/common/toast/container';
|
||||
import PadsSessionsContainer from '/imports/ui/components/pads/sessions/container';
|
||||
import PadsSessionsContainer from '/imports/ui/components/pads/pads-graphql/sessions/component';
|
||||
import WakeLockContainer from '../wake-lock/container';
|
||||
import NotificationsBarContainer from '../notifications-bar/container';
|
||||
import AudioContainer from '../audio/container';
|
||||
|
@ -24,7 +24,7 @@ import { isEqual } from 'radash';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
||||
import { LAYOUT_TYPE } from '/imports/ui/components/layout/enums';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { useMutation, useSubscription } from '@apollo/client';
|
||||
import { SET_MOBILE_FLAG } from '/imports/ui/core/graphql/mutations/userMutations';
|
||||
import { SET_SYNC_WITH_PRESENTER_LAYOUT, SET_LAYOUT_PROPS } from './mutations';
|
||||
|
||||
@ -36,8 +36,10 @@ import {
|
||||
import App from './component';
|
||||
import useToggleVoice from '../audio/audio-graphql/hooks/useToggleVoice';
|
||||
import useUserChangedLocalSettings from '../../services/settings/hooks/useUserChangedLocalSettings';
|
||||
import { PINNED_PAD_SUBSCRIPTION } from '../notes/notes-graphql/queries';
|
||||
|
||||
const CUSTOM_STYLE_URL = window.meetingClientSettings.public.app.customStyleUrl;
|
||||
const NOTES_CONFIG = window.meetingClientSettings.public.notes;
|
||||
|
||||
const endMeeting = (code, ejectedReason) => {
|
||||
Session.set('codeError', code);
|
||||
@ -63,7 +65,6 @@ const AppContainer = (props) => {
|
||||
pushLayoutMeeting,
|
||||
currentUserId,
|
||||
shouldShowScreenshare: propsShouldShowScreenshare,
|
||||
shouldShowSharedNotes,
|
||||
presentationRestoreOnUpdate,
|
||||
isModalOpen,
|
||||
meetingLayout,
|
||||
@ -94,6 +95,9 @@ const AppContainer = (props) => {
|
||||
const [setMeetingLayoutProps] = useMutation(SET_LAYOUT_PROPS);
|
||||
const toggleVoice = useToggleVoice();
|
||||
const setLocalSettings = useUserChangedLocalSettings();
|
||||
const { data: pinnedPadData } = useSubscription(PINNED_PAD_SUBSCRIPTION);
|
||||
const shouldShowSharedNotes = !!pinnedPadData
|
||||
&& pinnedPadData.sharedNotes[0]?.sharedNotesExtId === NOTES_CONFIG.id;
|
||||
|
||||
const setMobileUser = (mobile) => {
|
||||
setMobileFlag({
|
||||
@ -311,7 +315,6 @@ export default withTracker(() => {
|
||||
const AppSettings = Settings.application;
|
||||
const { selectedLayout, pushLayout } = AppSettings;
|
||||
const { viewScreenshare } = Settings.dataSaving;
|
||||
const shouldShowSharedNotes = MediaService.shouldShowSharedNotes();
|
||||
const shouldShowScreenshare = MediaService.shouldShowScreenshare();
|
||||
let customStyleUrl = getFromUserSettings('bbb_custom_style_url', false);
|
||||
|
||||
@ -351,7 +354,6 @@ export default withTracker(() => {
|
||||
darkTheme: AppSettings.darkTheme,
|
||||
shouldShowScreenshare,
|
||||
viewScreenshare,
|
||||
shouldShowSharedNotes,
|
||||
isLargeFont: Session.get('isLargeFont'),
|
||||
presentationRestoreOnUpdate: getFromUserSettings(
|
||||
'bbb_force_restore_presentation_on_new_events',
|
||||
|
@ -12,12 +12,13 @@ import { PluginsContext } from '/imports/ui/components/components-data/plugin-co
|
||||
import { PANELS } from '/imports/ui/components/layout/enums';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import useChat from '/imports/ui/core/hooks/useChat';
|
||||
import useHasUnreadNotes from '../notes/notes-graphql/hooks/useHasUnreadNotes';
|
||||
|
||||
const PUBLIC_CONFIG = window.meetingClientSettings.public;
|
||||
|
||||
const NavBarContainer = ({ children, ...props }) => {
|
||||
const { pluginsExtensibleAreasAggregatedState } = useContext(PluginsContext);
|
||||
const { unread, ...rest } = props;
|
||||
const unread = useHasUnreadNotes();
|
||||
|
||||
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
|
||||
const sidebarNavigation = layoutSelectInput((i) => i.sidebarNavigation);
|
||||
@ -69,7 +70,7 @@ const NavBarContainer = ({ children, ...props }) => {
|
||||
isExpanded,
|
||||
currentUserId: Auth.userID,
|
||||
pluginNavBarItems,
|
||||
...rest,
|
||||
...props,
|
||||
}}
|
||||
style={{ ...navBar }}
|
||||
>
|
||||
|
@ -5,6 +5,7 @@ import Service from './service';
|
||||
import MediaService from '/imports/ui/components/media/service';
|
||||
import { layoutSelectInput, layoutDispatch, layoutSelectOutput } from '../layout/context';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import NotesContainerGraphql from './notes-graphql/component';
|
||||
|
||||
const Container = ({ ...props }) => {
|
||||
const cameraDock = layoutSelectInput((i) => i.cameraDock);
|
||||
@ -27,7 +28,7 @@ const Container = ({ ...props }) => {
|
||||
}} />;
|
||||
};
|
||||
|
||||
export default withTracker(() => {
|
||||
withTracker(() => {
|
||||
const hasPermission = Service.hasPermission();
|
||||
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
|
||||
const shouldShowSharedNotesOnPresentationArea = MediaService.shouldShowSharedNotes();
|
||||
@ -38,3 +39,5 @@ export default withTracker(() => {
|
||||
isGridEnabled: Session.get('isGridEnabled') || false,
|
||||
};
|
||||
})(Container);
|
||||
|
||||
export default NotesContainerGraphql;
|
||||
|
@ -0,0 +1,282 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useMutation, useSubscription } from '@apollo/client';
|
||||
import { Session } from 'meteor/session';
|
||||
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
|
||||
import NotesService from '/imports/ui/components/notes/notes-graphql/service';
|
||||
import PadContainer from '/imports/ui/components/pads/container';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import Header from '/imports/ui/components/common/control-header/component';
|
||||
import NotesDropdown from './notes-dropdown/component';
|
||||
import { PANELS, ACTIONS, LAYOUT_TYPE } from '/imports/ui/components/layout/enums';
|
||||
import { isPresentationEnabled } from '/imports/ui/services/features';
|
||||
import { layoutSelectInput, layoutDispatch, layoutSelectOutput } from '/imports/ui/components/layout/context';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import useHasPermission from './hooks/useHasPermission';
|
||||
import Styled from './styles';
|
||||
import { PINNED_PAD_SUBSCRIPTION, PinnedPadSubscriptionResponse } from './queries';
|
||||
import { PIN_NOTES } from './mutations';
|
||||
import { EXTERNAL_VIDEO_STOP } from '/imports/ui/components/external-video-player/mutations';
|
||||
import {
|
||||
screenshareHasEnded,
|
||||
isScreenBroadcasting,
|
||||
} from '/imports/ui/components/screenshare/service';
|
||||
|
||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||
const NOTES_CONFIG = window.meetingClientSettings.public.notes;
|
||||
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
||||
const DELAY_UNMOUNT_SHARED_NOTES = window.meetingClientSettings.public.app.delayForUnmountOfSharedNote;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
hide: {
|
||||
id: 'app.notes.hide',
|
||||
description: 'Label for hiding shared notes button',
|
||||
},
|
||||
title: {
|
||||
id: 'app.notes.title',
|
||||
description: 'Title for the shared notes',
|
||||
},
|
||||
unpinNotes: {
|
||||
id: 'app.notes.notesDropdown.unpinNotes',
|
||||
description: 'Label for unpin shared notes button',
|
||||
},
|
||||
});
|
||||
|
||||
interface NotesContainerGraphqlProps {
|
||||
area: 'media' | undefined;
|
||||
layoutType: string;
|
||||
isToSharedNotesBeShow: boolean;
|
||||
}
|
||||
|
||||
interface NotesGraphqlProps extends NotesContainerGraphqlProps {
|
||||
hasPermission: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
layoutContextDispatch: (action: any) => void;
|
||||
isResizing: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
sidebarContent: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
sharedNotesOutput: any;
|
||||
amIPresenter: boolean;
|
||||
isRTL: boolean;
|
||||
shouldShowSharedNotesOnPresentationArea: boolean;
|
||||
handlePinSharedNotes: (pinned: boolean) => void;
|
||||
}
|
||||
|
||||
let timoutRef: NodeJS.Timeout | undefined;
|
||||
const sidebarContentToIgnoreDelay = ['captions'];
|
||||
|
||||
const NotesGraphql: React.FC<NotesGraphqlProps> = (props) => {
|
||||
const {
|
||||
hasPermission,
|
||||
isRTL,
|
||||
layoutContextDispatch,
|
||||
isResizing,
|
||||
area,
|
||||
layoutType,
|
||||
sidebarContent,
|
||||
sharedNotesOutput,
|
||||
amIPresenter,
|
||||
isToSharedNotesBeShow,
|
||||
shouldShowSharedNotesOnPresentationArea,
|
||||
handlePinSharedNotes,
|
||||
} = props;
|
||||
const [shouldRenderNotes, setShouldRenderNotes] = useState(false);
|
||||
const intl = useIntl();
|
||||
|
||||
const { isChrome } = browserInfo;
|
||||
const isOnMediaArea = area === 'media';
|
||||
const style = isOnMediaArea ? {
|
||||
position: 'absolute',
|
||||
...sharedNotesOutput,
|
||||
} : {};
|
||||
|
||||
const isHidden = (isOnMediaArea && (style.width === 0 || style.height === 0))
|
||||
|| (!isToSharedNotesBeShow
|
||||
&& !sidebarContentToIgnoreDelay.includes(sidebarContent.sidebarContentPanel))
|
||||
|| shouldShowSharedNotesOnPresentationArea;
|
||||
|
||||
if (isHidden && !isOnMediaArea) {
|
||||
style.padding = 0;
|
||||
style.display = 'none';
|
||||
}
|
||||
useEffect(() => {
|
||||
if (isToSharedNotesBeShow) {
|
||||
setShouldRenderNotes(true);
|
||||
clearTimeout(timoutRef);
|
||||
} else {
|
||||
timoutRef = setTimeout(() => {
|
||||
setShouldRenderNotes(false);
|
||||
}, (sidebarContentToIgnoreDelay.includes(sidebarContent.sidebarContentPanel)
|
||||
|| shouldShowSharedNotesOnPresentationArea)
|
||||
? 0 : DELAY_UNMOUNT_SHARED_NOTES);
|
||||
}
|
||||
return () => clearTimeout(timoutRef);
|
||||
}, [isToSharedNotesBeShow, sidebarContent.sidebarContentPanel]);
|
||||
// eslint-disable-next-line consistent-return
|
||||
useEffect(() => {
|
||||
if (
|
||||
isOnMediaArea
|
||||
&& (sidebarContent.isOpen || !isPresentationEnabled())
|
||||
&& (sidebarContent.sidebarContentPanel === PANELS.SHARED_NOTES || !isPresentationEnabled())
|
||||
) {
|
||||
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,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_NOTES_IS_PINNED,
|
||||
value: false,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: Session.get('presentationLastState'),
|
||||
});
|
||||
};
|
||||
} if (shouldShowSharedNotesOnPresentationArea) {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_NOTES_IS_PINNED,
|
||||
value: true,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderHeaderOnMedia = () => {
|
||||
return amIPresenter ? (
|
||||
<Styled.Header
|
||||
rightButtonProps={{
|
||||
'aria-label': intl.formatMessage(intlMessages.unpinNotes),
|
||||
'data-test': 'unpinNotes',
|
||||
icon: 'close',
|
||||
label: intl.formatMessage(intlMessages.unpinNotes),
|
||||
onClick: () => {
|
||||
handlePinSharedNotes(false);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
return (shouldRenderNotes || shouldShowSharedNotesOnPresentationArea) && (
|
||||
<Styled.Notes
|
||||
data-test="notes"
|
||||
isChrome={isChrome}
|
||||
style={style}
|
||||
>
|
||||
{!isOnMediaArea ? (
|
||||
// @ts-ignore Until everything in Typescript
|
||||
<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 handlePinSharedNotes={handlePinSharedNotes} />
|
||||
}
|
||||
/>
|
||||
) : renderHeaderOnMedia()}
|
||||
<PadContainer
|
||||
externalId={NotesService.ID}
|
||||
hasPermission={hasPermission}
|
||||
isResizing={isResizing}
|
||||
isRTL={isRTL}
|
||||
/>
|
||||
</Styled.Notes>
|
||||
);
|
||||
};
|
||||
|
||||
const NotesContainerGraphql: React.FC<NotesContainerGraphqlProps> = (props) => {
|
||||
const { area, layoutType, isToSharedNotesBeShow } = props;
|
||||
|
||||
const hasPermission = useHasPermission();
|
||||
const { data: pinnedPadData } = useSubscription<PinnedPadSubscriptionResponse>(PINNED_PAD_SUBSCRIPTION);
|
||||
const { data: currentUserData } = useCurrentUser((user) => ({
|
||||
presenter: user.presenter,
|
||||
}));
|
||||
const [pinSharedNotes] = useMutation(PIN_NOTES);
|
||||
|
||||
// @ts-ignore Until everything in Typescript
|
||||
const cameraDock = layoutSelectInput((i) => i.cameraDock);
|
||||
// @ts-ignore Until everything in Typescript
|
||||
const sharedNotesOutput = layoutSelectOutput((i) => i.sharedNotes);
|
||||
// @ts-ignore Until everything in Typescript
|
||||
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
|
||||
const { isResizing } = cameraDock;
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
const amIPresenter = !!currentUserData?.presenter;
|
||||
|
||||
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
|
||||
const shouldShowSharedNotesOnPresentationArea = !!pinnedPadData
|
||||
&& pinnedPadData.sharedNotes[0]?.sharedNotesExtId === NOTES_CONFIG.id;
|
||||
|
||||
const [stopExternalVideoShare] = useMutation(EXTERNAL_VIDEO_STOP);
|
||||
|
||||
const handlePinSharedNotes = (pinned: boolean) => {
|
||||
if (pinned) {
|
||||
stopExternalVideoShare();
|
||||
if (isScreenBroadcasting()) screenshareHasEnded();
|
||||
}
|
||||
pinSharedNotes({ variables: { pinned } });
|
||||
};
|
||||
|
||||
return (
|
||||
<NotesGraphql
|
||||
area={area}
|
||||
hasPermission={hasPermission}
|
||||
layoutContextDispatch={layoutContextDispatch}
|
||||
isResizing={isResizing}
|
||||
sidebarContent={sidebarContent}
|
||||
sharedNotesOutput={sharedNotesOutput}
|
||||
amIPresenter={amIPresenter}
|
||||
shouldShowSharedNotesOnPresentationArea={shouldShowSharedNotesOnPresentationArea}
|
||||
isRTL={isRTL}
|
||||
layoutType={layoutType}
|
||||
isToSharedNotesBeShow={isToSharedNotesBeShow}
|
||||
handlePinSharedNotes={handlePinSharedNotes}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectWbResizeEvent(NotesContainerGraphql);
|
@ -0,0 +1,24 @@
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
||||
|
||||
const ROLE_MODERATOR = window.meetingClientSettings.public.user.role_moderator;
|
||||
|
||||
const useHasPermission = () => {
|
||||
const { data: currentUserData } = useCurrentUser((u) => ({
|
||||
locked: u.locked,
|
||||
role: u.role,
|
||||
}));
|
||||
const { data: meetingData } = useMeeting((m) => ({
|
||||
lockSettings: m.lockSettings,
|
||||
}));
|
||||
|
||||
if (currentUserData?.role === ROLE_MODERATOR) return true;
|
||||
|
||||
if (currentUserData?.locked) {
|
||||
return !meetingData?.lockSettings?.disableNotes;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export default useHasPermission;
|
@ -0,0 +1,12 @@
|
||||
import useRev from '/imports/ui/components/pads/pads-graphql/hooks/useRev';
|
||||
import useNotesLastRev from './useNotesLastRev';
|
||||
|
||||
const NOTES_CONFIG = window.meetingClientSettings.public.notes;
|
||||
|
||||
const useHasUnreadNotes = () => {
|
||||
const { lastRev } = useNotesLastRev();
|
||||
const rev = useRev(NOTES_CONFIG.id);
|
||||
return rev > lastRev;
|
||||
};
|
||||
|
||||
export default useHasUnreadNotes;
|
@ -0,0 +1,16 @@
|
||||
import { useCallback } from 'react';
|
||||
import { makeVar, useReactiveVar } from '@apollo/client';
|
||||
|
||||
const notesLastRev = makeVar(0);
|
||||
|
||||
const useNotesLastRev = () => {
|
||||
const lastRev = useReactiveVar(notesLastRev);
|
||||
const setNotesLastRev = useCallback((rev: number) => notesLastRev(rev), []);
|
||||
|
||||
return {
|
||||
lastRev,
|
||||
setNotesLastRev,
|
||||
};
|
||||
};
|
||||
|
||||
export default useNotesLastRev;
|
@ -0,0 +1,14 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const PIN_NOTES = gql`
|
||||
mutation pinNotes($pinned: Boolean!) {
|
||||
sharedNotesSetPinned(
|
||||
sharedNotesExtId: notes,
|
||||
pinned: $pinned
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
PIN_NOTES,
|
||||
};
|
@ -0,0 +1,155 @@
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useQuery, useSubscription } from '@apollo/client';
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import Trigger from '/imports/ui/components/common/control-header/right/component';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import NotesService from '/imports/ui/components/notes/notes-graphql/service';
|
||||
import { uniqueId } from '/imports/utils/string-utils';
|
||||
import { layoutSelect } from '/imports/ui/components/layout/context';
|
||||
import { PROCESSED_PRESENTATIONS_SUBSCRIPTION } from '/imports/ui/components/whiteboard/queries';
|
||||
import Service from './service';
|
||||
import { GET_PAD_ID, GetPadIdQueryResponse } from './queries';
|
||||
|
||||
const DEBOUNCE_TIMEOUT = 15000;
|
||||
const NOTES_CONFIG = window.meetingClientSettings.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',
|
||||
},
|
||||
});
|
||||
|
||||
interface NotesDropdownContainerGraphqlProps {
|
||||
handlePinSharedNotes: (pinned: boolean) => void
|
||||
}
|
||||
|
||||
interface NotesDropdownGraphqlProps extends NotesDropdownContainerGraphqlProps {
|
||||
amIPresenter: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
presentations: any;
|
||||
isRTL: boolean;
|
||||
padId: string;
|
||||
}
|
||||
|
||||
const NotesDropdownGraphql: React.FC<NotesDropdownGraphqlProps> = (props) => {
|
||||
const {
|
||||
amIPresenter, presentations, handlePinSharedNotes, isRTL, padId,
|
||||
} = props;
|
||||
const [converterButtonDisabled, setConverterButtonDisabled] = useState(false);
|
||||
const intl = useIntl();
|
||||
|
||||
const getAvailableActions = () => {
|
||||
const uploadIcon = 'upload';
|
||||
const pinIcon = 'presentation';
|
||||
|
||||
const menuItems = [];
|
||||
|
||||
if (amIPresenter) {
|
||||
menuItems.push(
|
||||
{
|
||||
key: uniqueId('notes-option-'),
|
||||
icon: uploadIcon,
|
||||
dataTest: 'moveNotesToWhiteboard',
|
||||
label: intl.formatMessage(intlMessages.convertAndUploadLabel),
|
||||
disabled: converterButtonDisabled,
|
||||
onClick: () => {
|
||||
setConverterButtonDisabled(true);
|
||||
setTimeout(() => setConverterButtonDisabled(false), DEBOUNCE_TIMEOUT);
|
||||
return Service.convertAndUpload(presentations, padId);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (amIPresenter && NOTES_IS_PINNABLE) {
|
||||
menuItems.push(
|
||||
{
|
||||
key: uniqueId('notes-option-'),
|
||||
icon: pinIcon,
|
||||
dataTest: 'pinNotes',
|
||||
label: intl.formatMessage(intlMessages.pinNotes),
|
||||
onClick: () => {
|
||||
handlePinSharedNotes(true);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return menuItems;
|
||||
};
|
||||
|
||||
const actions = 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const NotesDropdownContainerGraphql: React.FC<NotesDropdownContainerGraphqlProps> = (props) => {
|
||||
const { handlePinSharedNotes } = props;
|
||||
const { data: currentUserData } = useCurrentUser((user) => ({
|
||||
presenter: user.presenter,
|
||||
}));
|
||||
const amIPresenter = !!currentUserData?.presenter;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const isRTL = layoutSelect((i: any) => i.isRTL);
|
||||
|
||||
const { data: presentationData } = useSubscription(PROCESSED_PRESENTATIONS_SUBSCRIPTION);
|
||||
const presentations = presentationData?.pres_presentation || [];
|
||||
|
||||
const { data: padIdData } = useQuery<GetPadIdQueryResponse>(
|
||||
GET_PAD_ID,
|
||||
{ variables: { externalId: NotesService.ID } },
|
||||
);
|
||||
const padId = padIdData?.sharedNotes?.[0]?.padId;
|
||||
|
||||
if (!padId) return null;
|
||||
|
||||
return (
|
||||
<NotesDropdownGraphql
|
||||
amIPresenter={amIPresenter}
|
||||
isRTL={isRTL}
|
||||
presentations={presentations}
|
||||
handlePinSharedNotes={handlePinSharedNotes}
|
||||
padId={padId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotesDropdownContainerGraphql;
|
@ -0,0 +1,21 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export interface GetPadIdQueryResponse {
|
||||
sharedNotes: Array<{
|
||||
padId: string;
|
||||
sharedNotesExtId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const GET_PAD_ID = gql`
|
||||
query getPadId($externalId: String!) {
|
||||
sharedNotes(where: { sharedNotesExtId: { _eq: $externalId } }) {
|
||||
padId
|
||||
sharedNotesExtId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
GET_PAD_ID,
|
||||
};
|
@ -0,0 +1,62 @@
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import PresentationUploaderService from '/imports/ui/components/presentation/presentation-uploader/service';
|
||||
import PadsService from '/imports/ui/components/pads/pads-graphql/service';
|
||||
import { UploadingPresentations } from '/imports/api/presentations';
|
||||
import { uniqueId } from '/imports/utils/string-utils';
|
||||
|
||||
const PADS_CONFIG = window.meetingClientSettings.public.pads;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async function convertAndUpload(presentations: any, padId: string) {
|
||||
let filename = 'Shared_Notes';
|
||||
const duplicates = presentations.filter(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(pres: any) => pres.filename?.startsWith(filename) || pres.name?.startsWith(filename),
|
||||
).length;
|
||||
|
||||
if (duplicates !== 0) { filename = `${filename}(${duplicates})`; }
|
||||
|
||||
const params = PadsService.getParams();
|
||||
const extension = 'pdf';
|
||||
filename = `${filename}.${extension}`;
|
||||
|
||||
UploadingPresentations.insert({
|
||||
id: uniqueId(filename),
|
||||
progress: 0,
|
||||
filename,
|
||||
lastModifiedUploader: false,
|
||||
upload: {
|
||||
done: false,
|
||||
error: false,
|
||||
},
|
||||
uploadTimestamp: new Date(),
|
||||
});
|
||||
|
||||
const exportUrl = Auth.authenticateURL(`${PADS_CONFIG.url}/p/${padId}/export/${extension}?${params}`);
|
||||
const sharedNotesAsFile = await fetch(exportUrl, { credentials: 'include' });
|
||||
|
||||
const data = await sharedNotesAsFile.blob();
|
||||
|
||||
const sharedNotesData = new File([data], filename, {
|
||||
type: data.type,
|
||||
});
|
||||
|
||||
PresentationUploaderService.handleSavePresentation([], false, {
|
||||
file: sharedNotesData,
|
||||
isDownloadable: false, // by default new presentations are set not to be downloadable
|
||||
isRemovable: true,
|
||||
filename: sharedNotesData.name,
|
||||
isCurrent: true,
|
||||
conversion: { done: false, error: false },
|
||||
upload: { done: false, error: false, progress: 0 },
|
||||
exportation: { isRunning: false, error: false },
|
||||
onConversion: () => { },
|
||||
onUpload: () => { },
|
||||
onProgress: () => { },
|
||||
onDone: () => { },
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
convertAndUpload,
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export interface PinnedPadSubscriptionResponse {
|
||||
sharedNotes: Array<{
|
||||
pinned: boolean;
|
||||
sharedNotesExtId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const PINNED_PAD_SUBSCRIPTION = gql`
|
||||
subscription isSharedNotesPinned {
|
||||
sharedNotes(where: { pinned: { _eq: true } }) {
|
||||
pinned
|
||||
sharedNotesExtId
|
||||
model
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default { PINNED_PAD_SUBSCRIPTION };
|
@ -0,0 +1,27 @@
|
||||
import { ACTIONS, PANELS } from '/imports/ui/components/layout/enums';
|
||||
import { isSharedNotesEnabled } from '/imports/ui/services/features';
|
||||
|
||||
const NOTES_CONFIG = window.meetingClientSettings.public.notes;
|
||||
|
||||
const isEnabled = () => isSharedNotesEnabled();
|
||||
|
||||
// @ts-ignore Until everything in Typescript
|
||||
const toggleNotesPanel = (sidebarContentPanel, layoutContextDispatch) => {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: sidebarContentPanel !== PANELS.SHARED_NOTES,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value:
|
||||
sidebarContentPanel === PANELS.SHARED_NOTES
|
||||
? PANELS.NONE
|
||||
: PANELS.SHARED_NOTES,
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
ID: NOTES_CONFIG.id,
|
||||
toggleNotesPanel,
|
||||
isEnabled,
|
||||
};
|
@ -0,0 +1,37 @@
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
mdPaddingX,
|
||||
} 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<{ isChrome: boolean }>`
|
||||
background-color: ${colorWhite};
|
||||
padding: ${mdPaddingX};
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
${({ isChrome }) => isChrome && `
|
||||
transform: translateZ(0);
|
||||
`}
|
||||
|
||||
@media ${smallOnly} {
|
||||
transform: none !important;
|
||||
&.no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Header = styled(CommonHeader)`
|
||||
padding-bottom: .2rem;
|
||||
`;
|
||||
|
||||
export default {
|
||||
Notes,
|
||||
Header,
|
||||
};
|
@ -3,10 +3,11 @@ import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Pad from './component';
|
||||
import Service from './service';
|
||||
import SessionsService from './sessions/service';
|
||||
import PadContainerGraphql from './pads-graphql/component';
|
||||
|
||||
const Container = ({ ...props }) => <Pad {...props} />;
|
||||
|
||||
export default withTracker((props) => {
|
||||
withTracker((props) => {
|
||||
const {
|
||||
externalId,
|
||||
hasPermission,
|
||||
@ -23,3 +24,5 @@ export default withTracker((props) => {
|
||||
hasSession,
|
||||
};
|
||||
})(Container);
|
||||
|
||||
export default PadContainerGraphql;
|
||||
|
@ -0,0 +1,112 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useMutation, useSubscription } from '@apollo/client';
|
||||
import { HAS_PAD_SUBSCRIPTION, HasPadSubscriptionResponse } from './queries';
|
||||
import { PAD_SESSION_SUBSCRIPTION, PadSessionSubscriptionResponse } from './sessions/queries';
|
||||
import { CREATE_SESSION } from './mutations';
|
||||
import Service from './service';
|
||||
import Styled from './styles';
|
||||
import PadContent from './content/component';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
hint: {
|
||||
id: 'app.pads.hint',
|
||||
description: 'Label for hint on how to escape iframe',
|
||||
},
|
||||
});
|
||||
|
||||
interface PadContainerGraphqlProps {
|
||||
externalId: string;
|
||||
hasPermission: boolean;
|
||||
isResizing: boolean;
|
||||
isRTL: boolean;
|
||||
}
|
||||
|
||||
interface PadGraphqlProps extends Omit<PadContainerGraphqlProps, 'hasPermission'> {
|
||||
hasSession: boolean;
|
||||
sessionIds: Array<string>;
|
||||
padId: string | undefined;
|
||||
}
|
||||
|
||||
const PadGraphql: React.FC<PadGraphqlProps> = (props) => {
|
||||
const {
|
||||
externalId,
|
||||
hasSession,
|
||||
isResizing,
|
||||
isRTL,
|
||||
sessionIds,
|
||||
padId,
|
||||
} = props;
|
||||
const [padURL, setPadURL] = useState<string | undefined>();
|
||||
const intl = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
if (!padId) {
|
||||
setPadURL(undefined);
|
||||
return;
|
||||
}
|
||||
setPadURL(Service.buildPadURL(padId, sessionIds));
|
||||
}, [isRTL, hasSession]);
|
||||
|
||||
if (!hasSession) {
|
||||
return <PadContent externalId={externalId} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Styled.Pad>
|
||||
<Styled.IFrame
|
||||
title="pad"
|
||||
src={padURL}
|
||||
aria-describedby="padEscapeHint"
|
||||
style={{
|
||||
pointerEvents: isResizing ? 'none' : 'inherit',
|
||||
}}
|
||||
/>
|
||||
<Styled.Hint
|
||||
id="padEscapeHint"
|
||||
aria-hidden
|
||||
>
|
||||
{intl.formatMessage(intlMessages.hint)}
|
||||
</Styled.Hint>
|
||||
</Styled.Pad>
|
||||
);
|
||||
};
|
||||
|
||||
const PadContainerGraphql: React.FC<PadContainerGraphqlProps> = (props) => {
|
||||
const {
|
||||
externalId,
|
||||
hasPermission,
|
||||
isRTL,
|
||||
isResizing,
|
||||
} = props;
|
||||
|
||||
const { data: hasPadData } = useSubscription<HasPadSubscriptionResponse>(
|
||||
HAS_PAD_SUBSCRIPTION,
|
||||
{ variables: { externalId } },
|
||||
);
|
||||
const { data: padSessionData } = useSubscription<PadSessionSubscriptionResponse>(PAD_SESSION_SUBSCRIPTION);
|
||||
const [createSession] = useMutation(CREATE_SESSION);
|
||||
|
||||
const sessionData = padSessionData?.sharedNotes_session ?? [];
|
||||
const session = sessionData.find((s) => s.sharedNotesExtId === externalId);
|
||||
const hasPad = !!hasPadData && hasPadData.sharedNotes.length > 0;
|
||||
const hasSession = !!session?.sessionId;
|
||||
const sessionIds = new Set<string>(sessionData.map((s) => s.sessionId));
|
||||
|
||||
if (hasPad && !hasSession && hasPermission) {
|
||||
createSession({ variables: { externalId } });
|
||||
}
|
||||
|
||||
return (
|
||||
<PadGraphql
|
||||
hasSession={hasSession}
|
||||
externalId={externalId}
|
||||
isRTL={isRTL}
|
||||
isResizing={isResizing}
|
||||
sessionIds={Array.from(sessionIds)}
|
||||
padId={session?.sharedNotes?.padId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PadContainerGraphql;
|
@ -0,0 +1,61 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSubscription } from '@apollo/client';
|
||||
import { patch } from '@mconf/bbb-diff';
|
||||
import Styled from './styles';
|
||||
import { GET_PAD_CONTENT_DIFF_STREAM, GetPadContentDiffStreamResponse } from './queries';
|
||||
|
||||
interface PadContentProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface PadContentContainerProps {
|
||||
externalId: string;
|
||||
}
|
||||
|
||||
const PadContent: React.FC<PadContentProps> = ({
|
||||
content,
|
||||
}) => {
|
||||
const contentSplit = content.split('<body>');
|
||||
const contentStyle = `
|
||||
<body>
|
||||
<style type="text/css">
|
||||
body {
|
||||
${Styled.contentText}
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
const contentWithStyle = [contentSplit[0], contentStyle, contentSplit[1]].join('');
|
||||
return (
|
||||
<Styled.Wrapper>
|
||||
<Styled.Iframe
|
||||
title="shared notes viewing mode"
|
||||
srcDoc={contentWithStyle}
|
||||
data-test="sharedNotesViewingMode"
|
||||
/>
|
||||
</Styled.Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const PadContentContainer: React.FC<PadContentContainerProps> = ({ externalId }) => {
|
||||
const [content, setContent] = useState('');
|
||||
const { data: contentDiffData } = useSubscription<GetPadContentDiffStreamResponse>(
|
||||
GET_PAD_CONTENT_DIFF_STREAM,
|
||||
{ variables: { externalId } },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!contentDiffData) return;
|
||||
const patches = contentDiffData.sharedNotes_diff_stream;
|
||||
const patchedContent = patches.reduce((currentContent, attribs) => patch(
|
||||
currentContent,
|
||||
{ start: attribs.start, end: attribs.end, text: attribs.diff },
|
||||
), content);
|
||||
setContent(patchedContent);
|
||||
}, [contentDiffData]);
|
||||
|
||||
return (
|
||||
<PadContent content={content} />
|
||||
);
|
||||
};
|
||||
|
||||
export default PadContentContainer;
|
3
bigbluebutton-html5/imports/ui/components/pads/pads-graphql/content/patch.d.ts
vendored
Normal file
3
bigbluebutton-html5/imports/ui/components/pads/pads-graphql/content/patch.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
declare module '@mconf/bbb-diff' {
|
||||
declare function patch (prevText: string, attribs: { start: number, end: number, text: string }): string;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export interface GetPadContentDiffStreamResponse {
|
||||
sharedNotes_diff_stream: Array<{
|
||||
start: number;
|
||||
end: number;
|
||||
diff: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const GET_PAD_CONTENT_DIFF_STREAM = gql`
|
||||
subscription GetPadContentDiffStream($externalId: String!) {
|
||||
sharedNotes_diff_stream(
|
||||
batch_size: 10,
|
||||
cursor: { initial_value: { rev: 0 } },
|
||||
where: { sharedNotesExtId: { _eq: $externalId } }
|
||||
) {
|
||||
start
|
||||
end
|
||||
diff
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
GET_PAD_CONTENT_DIFF_STREAM,
|
||||
};
|
@ -0,0 +1,54 @@
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
colorGray,
|
||||
colorGrayLightest,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const contentText = `
|
||||
font-family: Verdana, Arial, Helvetica, sans-serif;
|
||||
font-size: 15px;
|
||||
color: ${colorGray};
|
||||
bottom: 0;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
overflow-x: hidden;
|
||||
overflow-wrap: break-word;
|
||||
overflow-y: auto;
|
||||
padding-top: 1rem;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left:0;
|
||||
top: 0;
|
||||
white-space: normal;
|
||||
|
||||
|
||||
[dir="ltr"] & {
|
||||
padding-left: 1rem;
|
||||
padding-right: .5rem;
|
||||
}
|
||||
|
||||
[dir="rtl"] & {
|
||||
padding-left: .5rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Iframe = styled.iframe`
|
||||
border-width: 0;
|
||||
width: 100%;
|
||||
border-top: 1px solid ${colorGrayLightest};
|
||||
border-bottom: 1px solid ${colorGrayLightest};
|
||||
`;
|
||||
|
||||
export default {
|
||||
Wrapper,
|
||||
Iframe,
|
||||
contentText,
|
||||
};
|
@ -0,0 +1,37 @@
|
||||
import { gql, useSubscription } from '@apollo/client';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface GetPadLastRevResponse {
|
||||
sharedNotes: Array<{
|
||||
lastRev: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
const GET_PAD_LAST_REV = gql`
|
||||
subscription GetPadLastRev($externalId: String!) {
|
||||
sharedNotes(
|
||||
where: { sharedNotesExtId: { _eq: $externalId } }
|
||||
) {
|
||||
lastRev
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const useRev = (externalId: string) => {
|
||||
const [rev, setRev] = useState(0);
|
||||
const { data: padRevData } = useSubscription<GetPadLastRevResponse>(
|
||||
GET_PAD_LAST_REV,
|
||||
{ variables: { externalId } },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!padRevData) return;
|
||||
const pad = padRevData.sharedNotes[0];
|
||||
if (!pad) return;
|
||||
setRev(pad.lastRev);
|
||||
}, [padRevData]);
|
||||
|
||||
return rev;
|
||||
};
|
||||
|
||||
export default useRev;
|
@ -0,0 +1,13 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CREATE_SESSION = gql`
|
||||
mutation createSession($externalId: String!) {
|
||||
sharedNotesCreateSession(
|
||||
sharedNotesExtId: $externalId
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
CREATE_SESSION,
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export interface HasPadSubscriptionResponse {
|
||||
sharedNotes: Array<{
|
||||
sharedNotesExtId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const HAS_PAD_SUBSCRIPTION = gql`
|
||||
subscription hasPad($externalId: String!) {
|
||||
sharedNotes(
|
||||
where: { sharedNotesExtId: { _eq: $externalId } }
|
||||
) {
|
||||
sharedNotesExtId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
HAS_PAD_SUBSCRIPTION,
|
||||
};
|
@ -0,0 +1,55 @@
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import { PadsUpdates } from '/imports/api/pads';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
|
||||
const PADS_CONFIG = window.meetingClientSettings.public.pads;
|
||||
|
||||
const getLang = (): string => {
|
||||
// @ts-ignore While Meteor in the project
|
||||
const { locale } = Settings.application;
|
||||
return locale ? locale.toLowerCase() : '';
|
||||
};
|
||||
|
||||
const getParams = () => {
|
||||
const config = {
|
||||
lang: getLang(),
|
||||
rtl: document.documentElement.getAttribute('dir') === 'rtl',
|
||||
};
|
||||
|
||||
const params = Object.keys(config)
|
||||
.map((key) => `${key}=${encodeURIComponent(config[key as keyof typeof config])}`)
|
||||
.join('&');
|
||||
return params;
|
||||
};
|
||||
|
||||
const createGroup = (externalId: string, model: string, name: string) => makeCall('createGroup', externalId, model, name);
|
||||
|
||||
const buildPadURL = (padId: string, sessionIds: Array<string>) => {
|
||||
const params = getParams();
|
||||
const sessionIdsStr = sessionIds.join(',');
|
||||
const url = Auth.authenticateURL(
|
||||
`${PADS_CONFIG.url}/auth_session?padName=${padId}&sessionID=${sessionIdsStr}&${params}`,
|
||||
);
|
||||
return url;
|
||||
};
|
||||
|
||||
const getPadTail = (externalId: string) => {
|
||||
const updates = PadsUpdates.findOne(
|
||||
{
|
||||
meetingId: Auth.meetingID,
|
||||
externalId,
|
||||
}, { fields: { tail: 1 } },
|
||||
);
|
||||
|
||||
if (updates && updates.tail) return updates.tail;
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export default {
|
||||
createGroup,
|
||||
buildPadURL,
|
||||
getPadTail,
|
||||
getParams,
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import { useSubscription } from '@apollo/client';
|
||||
import { PAD_SESSION_SUBSCRIPTION, PadSessionSubscriptionResponse } from './queries';
|
||||
import Service from './service';
|
||||
|
||||
const PadSessionContainerGraphql = () => {
|
||||
const { data: padSessionData } = useSubscription<PadSessionSubscriptionResponse>(PAD_SESSION_SUBSCRIPTION);
|
||||
|
||||
if (padSessionData) {
|
||||
const sessions = new Set<string>();
|
||||
padSessionData.sharedNotes_session.forEach((session) => sessions.add(session.sessionId));
|
||||
Service.setCookie(Array.from(sessions));
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default PadSessionContainerGraphql;
|
@ -0,0 +1,29 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export interface PadSessionSubscriptionResponse {
|
||||
sharedNotes_session: Array<{
|
||||
sessionId: string;
|
||||
sharedNotesExtId: string;
|
||||
padId: string;
|
||||
sharedNotes: {
|
||||
padId: string;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export const PAD_SESSION_SUBSCRIPTION = gql`
|
||||
subscription padSession {
|
||||
sharedNotes_session {
|
||||
sessionId
|
||||
sharedNotesExtId
|
||||
padId
|
||||
sharedNotes {
|
||||
padId
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
PAD_SESSION_SUBSCRIPTION,
|
||||
};
|
@ -0,0 +1,13 @@
|
||||
const COOKIE_CONFIG = window.meetingClientSettings.public.pads.cookie;
|
||||
const PATH = COOKIE_CONFIG.path;
|
||||
const SAME_SITE = COOKIE_CONFIG.sameSite;
|
||||
const SECURE = COOKIE_CONFIG.secure;
|
||||
|
||||
const setCookie = (sessions: Array<string>) => {
|
||||
const sessionIds = sessions.join(',');
|
||||
document.cookie = `sessionID=${sessionIds}; path=${PATH}; SameSite=${SAME_SITE}; ${SECURE ? 'Secure' : ''}`;
|
||||
};
|
||||
|
||||
export default {
|
||||
setCookie,
|
||||
};
|
@ -0,0 +1,49 @@
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
smPaddingX,
|
||||
lgPaddingY,
|
||||
} from '/imports/ui/stylesheets/styled-components/general';
|
||||
import {
|
||||
colorGray,
|
||||
colorGrayLightest,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { fontSizeSmall } from '/imports/ui/stylesheets/styled-components/typography';
|
||||
|
||||
const Hint = styled.span`
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
@media (pointer: none) {
|
||||
visibility: visible;
|
||||
position: relative;
|
||||
color: ${colorGray};
|
||||
font-size: ${fontSizeSmall};
|
||||
font-style: italic;
|
||||
padding: ${smPaddingX} 0 0 ${smPaddingX};
|
||||
text-align: left;
|
||||
[dir="rtl"] & {
|
||||
padding-right: ${lgPaddingY} ${lgPaddingY} 0 0;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Pad = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const IFrame = styled.iframe`
|
||||
width: 100%;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
border-style: none;
|
||||
border-bottom: 1px solid ${colorGrayLightest};
|
||||
padding-bottom: 5px;
|
||||
`;
|
||||
|
||||
export default {
|
||||
Hint,
|
||||
Pad,
|
||||
IFrame,
|
||||
};
|
@ -4,6 +4,7 @@ import NotesService from '/imports/ui/components/notes/service';
|
||||
import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
|
||||
import UserNotes from './component';
|
||||
import { layoutSelectInput, layoutDispatch } from '../../../layout/context';
|
||||
import UserNotesContainerGraphql from '../../user-list-graphql/user-list-content/user-notes/component';
|
||||
|
||||
const UserNotesContainer = (props) => {
|
||||
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
|
||||
@ -12,7 +13,7 @@ const UserNotesContainer = (props) => {
|
||||
return <UserNotes {...{ layoutContextDispatch, sidebarContentPanel, ...props }} />;
|
||||
};
|
||||
|
||||
export default lockContextContainer(withTracker(({ userLocks }) => {
|
||||
lockContextContainer(withTracker(({ userLocks }) => {
|
||||
const shouldDisableNotes = userLocks.userNotes;
|
||||
return {
|
||||
unread: NotesService.hasUnreadNotes(),
|
||||
@ -20,3 +21,5 @@ export default lockContextContainer(withTracker(({ userLocks }) => {
|
||||
isPinned: NotesService.isSharedNotesPinned(),
|
||||
};
|
||||
})(UserNotesContainer));
|
||||
|
||||
export default UserNotesContainerGraphql;
|
||||
|
@ -0,0 +1,225 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSubscription } from '@apollo/client';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import Icon from '/imports/ui/components/common/icon/component';
|
||||
import NotesService from '/imports/ui/components/notes/notes-graphql/service';
|
||||
import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
|
||||
import { PANELS } from '/imports/ui/components/layout/enums';
|
||||
import { notify } from '/imports/ui/services/notification';
|
||||
import { layoutSelectInput, layoutDispatch } from '/imports/ui/components/layout/context';
|
||||
import {
|
||||
PINNED_PAD_SUBSCRIPTION,
|
||||
PinnedPadSubscriptionResponse,
|
||||
} from '/imports/ui/components/notes/notes-graphql/queries';
|
||||
import Styled from './styles';
|
||||
import { usePreviousValue } from '/imports/ui/components/utils/hooks';
|
||||
import useRev from '/imports/ui/components/pads/pads-graphql/hooks/useRev';
|
||||
import useNotesLastRev from '/imports/ui/components/notes/notes-graphql/hooks/useNotesLastRev';
|
||||
import useHasUnreadNotes from '/imports/ui/components/notes/notes-graphql/hooks/useHasUnreadNotes';
|
||||
|
||||
const NOTES_CONFIG = window.meetingClientSettings.public.notes;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
title: {
|
||||
id: 'app.userList.notesTitle',
|
||||
description: 'Title for the notes list',
|
||||
},
|
||||
pinnedNotification: {
|
||||
id: 'app.notes.pinnedNotification',
|
||||
description: 'Notification text for pinned shared notes',
|
||||
},
|
||||
sharedNotes: {
|
||||
id: 'app.notes.title',
|
||||
description: 'Title for the shared notes',
|
||||
},
|
||||
sharedNotesPinned: {
|
||||
id: 'app.notes.titlePinned',
|
||||
description: 'Title for the shared notes pinned',
|
||||
},
|
||||
unreadContent: {
|
||||
id: 'app.userList.notesListItem.unreadContent',
|
||||
description: 'Aria label for notes unread content',
|
||||
},
|
||||
locked: {
|
||||
id: 'app.notes.locked',
|
||||
description: '',
|
||||
},
|
||||
byModerator: {
|
||||
id: 'app.userList.byModerator',
|
||||
description: '',
|
||||
},
|
||||
disabled: {
|
||||
id: 'app.notes.disabled',
|
||||
description: 'Aria description for disabled notes button',
|
||||
},
|
||||
});
|
||||
|
||||
interface UserNotesGraphqlProps {
|
||||
isPinned: boolean,
|
||||
disableNotes: boolean,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
sidebarContentPanel: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
layoutContextDispatch: any,
|
||||
hasUnreadNotes: boolean,
|
||||
markNotesAsRead: () => void,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
toggleNotesPanel: (sidebarContentPanel: any, layoutContextDispatch: any) => void,
|
||||
isEnabled: () => boolean,
|
||||
}
|
||||
|
||||
interface UserNotesContainerGraphqlProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
userLocks: any;
|
||||
}
|
||||
|
||||
const UserNotesGraphql: React.FC<UserNotesGraphqlProps> = (props) => {
|
||||
const {
|
||||
isPinned,
|
||||
disableNotes,
|
||||
sidebarContentPanel,
|
||||
layoutContextDispatch,
|
||||
isEnabled,
|
||||
hasUnreadNotes,
|
||||
markNotesAsRead,
|
||||
toggleNotesPanel,
|
||||
} = props;
|
||||
const [unread, setUnread] = useState(false);
|
||||
const [pinWasNotified, setPinWasNotified] = useState(false);
|
||||
const intl = useIntl();
|
||||
const prevSidebarContentPanel = usePreviousValue(sidebarContentPanel);
|
||||
const prevIsPinned = usePreviousValue(isPinned);
|
||||
|
||||
useEffect(() => {
|
||||
setUnread(hasUnreadNotes);
|
||||
}, []);
|
||||
|
||||
if (isPinned && !pinWasNotified) {
|
||||
notify(intl.formatMessage(intlMessages.pinnedNotification), 'info', 'copy', { pauseOnFocusLoss: false });
|
||||
setPinWasNotified(true);
|
||||
}
|
||||
|
||||
const notesOpen = sidebarContentPanel === PANELS.SHARED_NOTES && !isPinned;
|
||||
const notesClosed = (prevSidebarContentPanel === PANELS.SHARED_NOTES
|
||||
&& sidebarContentPanel !== PANELS.SHARED_NOTES)
|
||||
|| (prevIsPinned && !isPinned);
|
||||
|
||||
if ((notesOpen || notesClosed) && unread) {
|
||||
markNotesAsRead();
|
||||
}
|
||||
if (!unread && hasUnreadNotes) {
|
||||
setUnread(true);
|
||||
}
|
||||
if (unread && !hasUnreadNotes) {
|
||||
setUnread(false);
|
||||
}
|
||||
if (prevIsPinned && !isPinned && pinWasNotified) {
|
||||
setPinWasNotified(false);
|
||||
}
|
||||
|
||||
const renderNotes = () => {
|
||||
let notification = null;
|
||||
if (unread && !isPinned) {
|
||||
notification = (
|
||||
<Styled.UnreadMessages aria-label={intl.formatMessage(intlMessages.unreadContent)}>
|
||||
<Styled.UnreadMessagesText aria-hidden="true">
|
||||
···
|
||||
</Styled.UnreadMessagesText>
|
||||
</Styled.UnreadMessages>
|
||||
);
|
||||
}
|
||||
|
||||
const showTitle = isPinned ? intl.formatMessage(intlMessages.sharedNotesPinned)
|
||||
: intl.formatMessage(intlMessages.sharedNotes);
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Styled.ListItem
|
||||
aria-label={showTitle}
|
||||
aria-describedby="lockedNotes"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => toggleNotesPanel(sidebarContentPanel, layoutContextDispatch)}
|
||||
// @ts-ignore
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
toggleNotesPanel(sidebarContentPanel, layoutContextDispatch);
|
||||
}
|
||||
}}
|
||||
as={isPinned ? 'button' : 'div'}
|
||||
disabled={isPinned}
|
||||
$disabled={isPinned}
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
<Icon iconName="copy" />
|
||||
<div aria-hidden>
|
||||
<Styled.NotesTitle data-test="sharedNotes">
|
||||
{ showTitle }
|
||||
</Styled.NotesTitle>
|
||||
{disableNotes
|
||||
? (
|
||||
<Styled.NotesLock>
|
||||
{/* @ts-ignore */}
|
||||
<Icon iconName="lock" />
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
if (!isEnabled()) return null;
|
||||
|
||||
return (
|
||||
<Styled.Messages>
|
||||
<Styled.Container>
|
||||
<Styled.SmallTitle data-test="notesTitle">
|
||||
{intl.formatMessage(intlMessages.title)}
|
||||
</Styled.SmallTitle>
|
||||
</Styled.Container>
|
||||
<Styled.ScrollableList>
|
||||
<Styled.List>
|
||||
{renderNotes()}
|
||||
</Styled.List>
|
||||
</Styled.ScrollableList>
|
||||
</Styled.Messages>
|
||||
);
|
||||
};
|
||||
|
||||
const UserNotesContainerGraphql: React.FC<UserNotesContainerGraphqlProps> = (props) => {
|
||||
const { userLocks } = props;
|
||||
const disableNotes = userLocks.userNotes;
|
||||
const { data: pinnedPadData } = useSubscription<PinnedPadSubscriptionResponse>(PINNED_PAD_SUBSCRIPTION);
|
||||
const isPinned = !!pinnedPadData && pinnedPadData.sharedNotes[0]?.sharedNotesExtId === NOTES_CONFIG.id;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const sidebarContent = layoutSelectInput((i: any) => i.sidebarContent);
|
||||
const { sidebarContentPanel } = sidebarContent;
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
|
||||
const rev = useRev(NotesService.ID);
|
||||
const { setNotesLastRev } = useNotesLastRev();
|
||||
|
||||
const hasUnreadNotes = useHasUnreadNotes();
|
||||
const markNotesAsRead = () => setNotesLastRev(rev);
|
||||
|
||||
return (
|
||||
<UserNotesGraphql
|
||||
disableNotes={disableNotes}
|
||||
isPinned={isPinned}
|
||||
layoutContextDispatch={layoutContextDispatch}
|
||||
sidebarContentPanel={sidebarContentPanel}
|
||||
hasUnreadNotes={hasUnreadNotes}
|
||||
markNotesAsRead={markNotesAsRead}
|
||||
toggleNotesPanel={NotesService.toggleNotesPanel}
|
||||
isEnabled={NotesService.isEnabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default lockContextContainer(UserNotesContainerGraphql);
|
@ -0,0 +1,56 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Styled from '/imports/ui/components/user-list/styles';
|
||||
import StyledContent from '/imports/ui/components/user-list/user-list-content/styles';
|
||||
import { colorGray } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import {
|
||||
fontSizeSmall,
|
||||
fontSizeSmaller,
|
||||
fontSizeXS,
|
||||
} from '/imports/ui/stylesheets/styled-components/typography';
|
||||
|
||||
const UnreadMessages = styled(StyledContent.UnreadMessages)``;
|
||||
|
||||
const UnreadMessagesText = styled(StyledContent.UnreadMessagesText)``;
|
||||
|
||||
const ListItem = styled(StyledContent.ListItem)`
|
||||
i{ left: 4px; }
|
||||
`;
|
||||
|
||||
const NotesTitle = styled.div`
|
||||
font-weight: 400;
|
||||
font-size: ${fontSizeSmall};
|
||||
`;
|
||||
|
||||
const NotesLock = styled.div`
|
||||
font-weight: 200;
|
||||
font-size: ${fontSizeSmaller};
|
||||
color: ${colorGray};
|
||||
|
||||
> i {
|
||||
font-size: ${fontSizeXS};
|
||||
}
|
||||
`;
|
||||
|
||||
const Messages = styled(Styled.Messages)``;
|
||||
|
||||
const Container = styled(StyledContent.Container)``;
|
||||
|
||||
const SmallTitle = styled(Styled.SmallTitle)``;
|
||||
|
||||
const ScrollableList = styled(StyledContent.ScrollableList)``;
|
||||
|
||||
const List = styled(StyledContent.List)``;
|
||||
|
||||
export default {
|
||||
UnreadMessages,
|
||||
UnreadMessagesText,
|
||||
ListItem,
|
||||
NotesTitle,
|
||||
NotesLock,
|
||||
Messages,
|
||||
Container,
|
||||
SmallTitle,
|
||||
ScrollableList,
|
||||
List,
|
||||
};
|
@ -3,11 +3,11 @@ import { useEffect, useRef } from 'react';
|
||||
/**
|
||||
* Custom hook to get previous value. It can be used,
|
||||
* for example, to get previous props or state.
|
||||
* @param {*} value
|
||||
* @param {*} value Value to be tracked
|
||||
* @returns The previous value.
|
||||
*/
|
||||
export const usePreviousValue = (value) => {
|
||||
const ref = useRef();
|
||||
export const usePreviousValue = <T = unknown>(value: T) => {
|
||||
const ref = useRef<T>();
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
Loading…
Reference in New Issue
Block a user