Merge pull request #19448 from JoVictorNunes/migrate-pads

refactor(notes / captions): migrate pads from Meteor to GraphQL
This commit is contained in:
Ramón Souza 2024-04-02 15:40:38 -03:00 committed by GitHub
commit 4885b4b5d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 1467 additions and 107 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
declare module '@mconf/bbb-diff' {
declare function patch (prevText: string, attribs: { start: number, end: number, text: string }): string;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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