feat(generic-content): add sidekick type

Adds 'sidekick' type of generic content. This type allows rendering
generic content in the sidekick panel, in addition to the existing 'main'
type, which renders generic content over the presentation area.
Each generic sidekick content set through plugins is automatically
associated with a button in the navigation bar to toggle its panel.
This commit is contained in:
Arthurk12 2024-06-10 18:16:11 -03:00
parent 95d823951f
commit a1dee317f3
10 changed files with 254 additions and 1 deletions

View File

@ -0,0 +1,45 @@
import React from 'react';
import Styled from './styles';
import { GenericSidekickContentProps } from '../types';
import GenericContentItem from '../generic-content-item/component';
import { layoutDispatch } from '/imports/ui/components/layout/context';
import { PANELS, ACTIONS } from '/imports/ui/components/layout/enums';
const GenericSidekickContent: React.FC<GenericSidekickContentProps> = ({
renderFunction,
genericContentId,
genericContentLabel,
}) => {
const layoutContextDispatch = layoutDispatch();
return (
<Styled.Container
data-test={genericContentId}
>
<Styled.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': `hide_${genericContentId}`,
'aria-label': genericContentLabel,
label: genericContentLabel,
}}
customRightButton={null}
/>
<GenericContentItem
key={genericContentId}
renderFunction={renderFunction}
/>
</Styled.Container>
);
};
export default GenericSidekickContent;

View File

@ -0,0 +1,47 @@
import React, { useContext } from 'react';
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
import { GenericContentType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/extensible-areas/generic-content/enums';
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
import { PANELS } from '/imports/ui/components/layout/enums';
import GenericSidekickContent from './component';
import { GenericSidekickContentContainerProps } from '../types';
import logger from '/imports/startup/client/logger';
const GenericSidekickContentContainer: React.FC<GenericSidekickContentContainerProps> = (props: GenericSidekickContentContainerProps) => {
const { genericSidekickContentId } = props;
const genericSidekickContentIdIsolated = genericSidekickContentId.replace(PANELS.GENERIC_SIDEKICK_CONTENT, "");
const {
pluginsExtensibleAreasAggregatedState,
} = useContext(PluginsContext);
let genericSidekickContentExtensibleArea = [] as PluginSdk.GenericSidekickContent[];
if (pluginsExtensibleAreasAggregatedState.genericContents) {
const genericMainContent = pluginsExtensibleAreasAggregatedState.genericContents.filter((g) => g.type === GenericContentType.SIDEKICK_CONTENT) as PluginSdk.GenericSidekickContent[];
genericSidekickContentExtensibleArea = [...genericMainContent];
}
if (genericSidekickContentExtensibleArea.length === 0) return null;
const pickedGenericSidekickContent = genericSidekickContentExtensibleArea.filter((gsc) => gsc.id === genericSidekickContentIdIsolated)[0];
if (!pickedGenericSidekickContent) {
logger.error({
logCode: 'generic_sidekick_content_not_found',
extraInfo: {
genericSidekickContentId,
genericSidekickContentIdIsolated,
},
}, `Generic sidekick content with id ${genericSidekickContentIdIsolated} not found`);
}
return (
<GenericSidekickContent
genericContentId={pickedGenericSidekickContent.id}
renderFunction={pickedGenericSidekickContent.contentFunction}
genericContentLabel={pickedGenericSidekickContent.name}
/>
);
};
export default GenericSidekickContentContainer;

View File

@ -0,0 +1,33 @@
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 Container = styled.div`
background-color: ${colorWhite};
padding: ${mdPaddingX};
display: flex;
flex-grow: 1;
flex-direction: column;
overflow: hidden;
height: 100%;
@media ${smallOnly} {
transform: none !important;
&.no-padding {
padding: 0;
}
}
`;
const Header = styled(CommonHeader)`
padding-bottom: .2rem;
`;
export default {
Container,
Header,
};

View File

@ -12,3 +12,12 @@ export interface GenericMainContentProps {
renderFunctionComponents: GenericMainContent[];
}
export interface GenericSidekickContentContainerProps {
genericSidekickContentId: string;
}
export interface GenericSidekickContentProps {
genericContentId: string;
renderFunction: (element: HTMLElement) => void;
genericContentLabel: string;
}

View File

@ -133,5 +133,6 @@ export const PANELS = {
SHARED_NOTES: 'shared-notes',
TIMER: 'timer',
WAITING_USERS: 'waiting-users',
GENERIC_SIDEKICK_CONTENT: 'generic-sidekick-content',
NONE: 'none',
};

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import Resizable from 're-resizable';
import { GenericContentType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/extensible-areas/generic-content/enums';
import { ACTIONS, PANELS } from '../layout/enums';
import ChatContainer from '/imports/ui/components/chat/chat-graphql/component';
import NotesContainer from '/imports/ui/components/notes/component';
@ -11,6 +12,7 @@ import GuestUsersManagementPanel from '/imports/ui/components/waiting-users/wait
import Styled from './styles';
import ErrorBoundary from '/imports/ui/components/common/error-boundary/component';
import FallbackView from '/imports/ui/components/common/fallback-errors/fallback-view/component';
import GenericSidekickContentContainer from '/imports/ui/components/generic-content/generic-sidekick-content/container';
const propTypes = {
top: PropTypes.number.isRequired,
@ -157,6 +159,11 @@ const SidebarContent = (props) => {
/>
</Styled.Poll>
)}
{sidebarContentPanel.includes(PANELS.GENERIC_SIDEKICK_CONTENT) && (
<GenericSidekickContentContainer
genericSidekickContentId={sidebarContentPanel}
/>
)}
</Resizable>
);
};

View File

@ -10,6 +10,8 @@ import UserPollsContainer from './user-polls/container';
import BreakoutRoomContainer from './breakout-room/container';
import { isChatEnabled } from '/imports/ui/services/features';
import UserTitleContainer from '../user-list-graphql/user-participants-title/component';
import { GenericSidekickContent } from 'bigbluebutton-html-plugin-sdk';
import GenericSidekickContentNavButtonContainer from './generic-sidekick-content-button/container';
const propTypes = {
currentUser: PropTypes.shape({
@ -46,9 +48,10 @@ class UserContent extends PureComponent {
{isTimerActive && <TimerContainer isModerator={currentUser?.role === ROLE_MODERATOR} />}
{currentUser?.role === ROLE_MODERATOR ? (
<GuestPanelOpenerContainer />
) : null}
) : null}
<UserPollsContainer isPresenter={currentUser?.presenter} />
<BreakoutRoomContainer />
<GenericSidekickContentNavButtonContainer />
<UserTitleContainer />
<UserListParticipants compact={compact} />
</Styled.Content>

View File

@ -0,0 +1,59 @@
import React, { useContext } from 'react';
import PluginSdk from 'bigbluebutton-html-plugin-sdk';
import { GenericContentType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/extensible-areas/generic-content/enums';
import Icon from '/imports/ui/components/common/icon/component';
import Styled from './styles';
import { ACTIONS, PANELS } from '/imports/ui/components/layout/enums';
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
const GenericSidekickContentNavButton = ({ sidebarContentPanel, layoutContextDispatch }) => {
const { pluginsExtensibleAreasAggregatedState } = useContext(PluginsContext);
let genericSidekickContentExtensibleArea = [];
if (pluginsExtensibleAreasAggregatedState.genericContents) {
const genericMainContent = pluginsExtensibleAreasAggregatedState.genericContents.filter((g) => g.type === GenericContentType.SIDEKICK_CONTENT);
genericSidekickContentExtensibleArea = [...genericMainContent];
}
const genericSidekickContentId = (id) => PANELS.GENERIC_SIDEKICK_CONTENT + id;
if (genericSidekickContentExtensibleArea.length === 0) return null;
const genericSidekickContentNavButtons = genericSidekickContentExtensibleArea.map((gsc) => (
<Styled.Section>
<Styled.Container>
<Styled.SmallTitle>
{gsc.section}
</Styled.SmallTitle>
</Styled.Container>
<Styled.ScrollableList>
<Styled.List>
<Styled.ListItem
role="button"
tabIndex={0}
active={sidebarContentPanel === genericSidekickContentId(gsc.id)}
onClick={() => {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: sidebarContentPanel !== genericSidekickContentId(gsc.id),
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: sidebarContentPanel === genericSidekickContentId(gsc.id)
? PANELS.NONE
: genericSidekickContentId(gsc.id),
});
}}
>
<Icon iconName={gsc.buttonIcon} />
<span>
{gsc.name}
</span>
</Styled.ListItem>
</Styled.List>
</Styled.ScrollableList>
</Styled.Section>
));
return <>{genericSidekickContentNavButtons}</>
}
export default GenericSidekickContentNavButton;

View File

@ -0,0 +1,22 @@
import React from 'react';
import GenericSidekickContentNavButton from './component';
import { layoutSelectInput, layoutDispatch } from '/imports/ui/components/layout/context';
const GenericSidekickContentNavButtonContainer = (props) => {
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
const { sidebarContentPanel } = sidebarContent;
const layoutContextDispatch = layoutDispatch();
return (
<GenericSidekickContentNavButton {
...{
layoutContextDispatch,
sidebarContentPanel,
...props,
}
}
/>
);
};
export default GenericSidekickContentNavButtonContainer;

View File

@ -0,0 +1,27 @@
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';
const ListItem = styled(StyledContent.ListItem)`
i{ left: 4px; }
`;
const Section = 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 {
ListItem,
Section,
Container,
SmallTitle,
ScrollableList,
List,
};