Skip to content

Commit

Permalink
Merge pull request #257 from nordeck/nic/feat/PB-3562-Avatars-of-coll…
Browse files Browse the repository at this point in the history
…aborators-are-shown-in-every-mode

Avatars of collaborators are shown in every mode
  • Loading branch information
maheichyk authored Dec 6, 2023
2 parents 3338d79 + 363f555 commit a2fdeb4
Show file tree
Hide file tree
Showing 13 changed files with 163 additions and 94 deletions.
5 changes: 5 additions & 0 deletions .changeset/eight-lions-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@nordeck/matrix-neoboard-widget': minor
---

Show collaborators avatars in presentation mode
4 changes: 2 additions & 2 deletions public/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,6 @@
"presentBar": {
"disableEditing": "Bearbeitung sperren",
"enableEditing": "Bearbeitung freigeben",
"isPresenting": "präsentiert",
"isPresentingUser": "{{displayName}} präsentiert",
"startPresentation": "Präsentation starten",
"stopPresentation": "Präsentation beenden",
"title": "Präsentieren"
Expand Down Expand Up @@ -202,6 +200,8 @@
"toolbar": {
"toolbarAvatar": {
"label_you": "{{displayName}} (Du)",
"labelPresenter": "{{displayName}} präsentiert",
"labelPresenting_you": "{{displayName}} (Du) präsentiert",
"more": "+{{furtherCount}}"
}
},
Expand Down
4 changes: 2 additions & 2 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,6 @@
"presentBar": {
"disableEditing": "Disable editing",
"enableEditing": "Enable editing",
"isPresenting": "is presenting",
"isPresentingUser": "{{displayName}} is presenting",
"startPresentation": "Start presentation",
"stopPresentation": "Stop presentation",
"title": "Present"
Expand Down Expand Up @@ -202,6 +200,8 @@
"toolbar": {
"toolbarAvatar": {
"label_you": "{{displayName}} (You)",
"labelPresenter": "{{displayName}} is presenting",
"labelPresenting_you": "{{displayName}} (You) are presenting",
"more": "+{{furtherCount}}"
}
},
Expand Down
3 changes: 1 addition & 2 deletions src/components/CollaborationBar/CollaborationBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export function CollaborationBar() {
const { state: presentationState } = usePresentationMode();
const isEditEnabled =
presentationState.type === 'idle' || presentationState.isEditMode;
const isViewingPresentation = presentationState.type === 'presentation';
const toolbarTitle = t('collaborationBar.title', 'Collaboration');

return (
Expand All @@ -35,7 +34,7 @@ export function CollaborationBar() {
data-guided-tour-target="collaborationbar"
>
{isEditEnabled && <ShowCollaboratorsCursorsToggle />}
{!isViewingPresentation && <Collaborators />}
<Collaborators />
</Toolbar>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ describe('<Collaborators>', () => {
let Wrapper: ComponentType<PropsWithChildren<{}>>;
let whiteboardManager: jest.Mocked<WhiteboardManager>;
let statistics: WhiteboardStatistics;
let setPresentationMode: (enable: boolean) => void;

beforeEach(() => {
({ whiteboardManager } = mockWhiteboardManager());
({ whiteboardManager, setPresentationMode } = mockWhiteboardManager());

widgetApi.mockSendStateEvent(mockRoomMember({ state_key: '@user-id' }));
widgetApi.mockSendStateEvent(
Expand Down Expand Up @@ -148,6 +149,20 @@ describe('<Collaborators>', () => {
);
});

it('should always display the presenter in presentation mode', async () => {
setPresentationMode(true);

render(<Collaborators />, { wrapper: Wrapper });

const group = screen.getByRole('group', { name: 'Collaborators' });

expect(
within(group).getByRole('button', {
name: '@user-alice is presenting',
}),
).toBeInTheDocument();
});

it('should have no accessibility violations', async () => {
const { container } = render(<Collaborators />, { wrapper: Wrapper });

Expand Down
27 changes: 24 additions & 3 deletions src/components/CollaborationBar/Collaborators/Collaborators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { useTranslation } from 'react-i18next';
import {
ActiveWhiteboardMember,
useActiveWhiteboardMembers,
usePresentationMode,
} from '../../../state';
import { useUserDetails } from '../../../store';
import { CollaboratorAvatar } from '../../common/CollaboratorAvatar';
Expand All @@ -41,14 +42,25 @@ import { orderMembersByState } from './orderMembersByState';

export function Collaborators() {
const { t } = useTranslation();
const { state } = usePresentationMode();
const ownUserId = useWidgetApi().widgetParameters.userId;
const presenterUserId =
state.type === 'presentation'
? state.presenterUserId
: state.type === 'presenting'
? ownUserId
: undefined;

if (!ownUserId) {
throw new Error('Unknown user id');
}

const activeMembers = useActiveWhiteboardMembers();
const orderedMembers = orderMembersByState(activeMembers, ownUserId);
const orderedMembers = orderMembersByState(
activeMembers,
ownUserId,
presenterUserId,
);
const maxAvatars = 5;
const furtherMembers = orderedMembers.slice(maxAvatars);

Expand Down Expand Up @@ -91,8 +103,17 @@ export function Collaborators() {
/>
}
>
{orderedMembers.map((m) => (
<ToolbarAvatar key={m.userId} member={m} />
{orderedMembers.map((m, index) => (
<ToolbarAvatar
key={m.userId}
member={m}
presenter={index === 0 && presenterUserId !== undefined}
sx={
index === 1 && presenterUserId !== undefined
? { marginLeft: 2 }
: undefined
}
/>
))}
</ToolbarAvatarGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { orderMembersByState } from './orderMembersByState';

describe('orderMembersByState', () => {
it('should order own user first, followed by connected and recently active users', () => {
it('should order own user first, followed by connected users', () => {
expect(
orderMembersByState(
[
Expand All @@ -43,4 +43,49 @@ describe('orderMembersByState', () => {
{ userId: '@user-id' },
]);
});

it('should order presenter user first then own user, followed by connected users', () => {
expect(
orderMembersByState(
[
{ userId: '@user-alice' },
{ userId: '@user-bob' },
{ userId: '@user-charlie' },
{ userId: '@user-dave' },
{ userId: '@user-id' },
],
'@user-id',
'@user-paul',
),
).toEqual([
{ userId: '@user-paul' },
{ userId: '@user-id' },
{ userId: '@user-alice' },
{ userId: '@user-bob' },
{ userId: '@user-charlie' },
{ userId: '@user-dave' },
]);
});

it('should order own user when he is presenter, followed by connected users', () => {
expect(
orderMembersByState(
[
{ userId: '@user-alice' },
{ userId: '@user-bob' },
{ userId: '@user-charlie' },
{ userId: '@user-dave' },
{ userId: '@user-id' },
],
'@user-id',
'@user-id',
),
).toEqual([
{ userId: '@user-id' },
{ userId: '@user-alice' },
{ userId: '@user-bob' },
{ userId: '@user-charlie' },
{ userId: '@user-dave' },
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ import { ActiveWhiteboardMember } from '../../../state';
export function orderMembersByState(
activeMembers: ActiveWhiteboardMember[],
ownUserId: string,
presenterUserId?: string,
): ActiveWhiteboardMember[] {
const ownUser = { userId: ownUserId };
const connectedUsers = activeMembers.filter((u) => u.userId !== ownUserId);

return [ownUser, ...connectedUsers];
const firstUsers =
presenterUserId === undefined || presenterUserId === ownUserId
? [ownUser]
: [{ userId: presenterUserId }, ownUser];
const connectedUsers = activeMembers.filter(
(u) => u.userId !== ownUserId && u.userId !== presenterUserId,
);

return [...firstUsers, ...connectedUsers];
}
6 changes: 2 additions & 4 deletions src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,6 @@ function ContentArea() {
const isViewingPresentation = presentationState.type === 'presentation';
const isViewingPresentationInEditMode =
isViewingPresentation && presentationState.isEditMode;
const isEditEnabled =
presentationState.type === 'idle' || presentationState.isEditMode;

return (
<>
Expand All @@ -129,8 +127,8 @@ function ContentArea() {
top={(theme) => theme.spacing(1)}
>
{!isViewingPresentation && <BoardBar />}
{(isEditEnabled || !isViewingPresentation) && <CollaborationBar />}
<PresentBar />
<CollaborationBar />
{!isViewingPresentation && <PresentBar />}
</ToolbarContainer>

<WhiteboardHost />
Expand Down
18 changes: 0 additions & 18 deletions src/components/PresentBar/PresentBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,6 @@ describe('<PresentBar/>', () => {

const { container } = render(<PresentBar />, { wrapper: Wrapper });

expect(
await screen.findByRole('button', { name: 'Alice is presenting' }),
).toBeInTheDocument();

expect(await axe(container)).toHaveNoViolations();
});

Expand Down Expand Up @@ -153,20 +149,6 @@ describe('<PresentBar/>', () => {
).toBeInTheDocument();
});

it('should show the active presenter', async () => {
setPresentationMode(true);

render(<PresentBar />, { wrapper: Wrapper });

const toolbar = screen.getByRole('toolbar', { name: 'Present' });

expect(
await within(toolbar).findByRole('button', {
name: 'Alice is presenting',
}),
).toBeInTheDocument();
});

it('should change to the next slide', async () => {
render(<PresentBar />, { wrapper: Wrapper });

Expand Down
40 changes: 1 addition & 39 deletions src/components/PresentBar/PresentBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,14 @@ import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import LockOpenIcon from '@mui/icons-material/LockOpen';
import PresentToAllIcon from '@mui/icons-material/PresentToAll';
import { Box } from '@mui/material';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
PresentationState,
useActiveSlide,
useActiveWhiteboardInstance,
usePresentationMode,
} from '../../state';
import { useUserDetails } from '../../store';
import {
Toolbar,
ToolbarAvatar,
ToolbarButton,
ToolbarToggle,
} from '../common/Toolbar';
import { Toolbar, ToolbarButton, ToolbarToggle } from '../common/Toolbar';

export function PresentBar() {
const { t } = useTranslation();
Expand Down Expand Up @@ -121,36 +113,6 @@ export function PresentBar() {
/>
</>
)}

{state.type === 'presentation' && <PresenterAvatar state={state} />}
</Toolbar>
);
}

function PresenterAvatar({
state,
}: {
state: Extract<PresentationState, { type: 'presentation' }>;
}) {
const { t } = useTranslation();

const { getUserDisplayName } = useUserDetails();
const displayName = getUserDisplayName(state.presenterUserId);

const presenterTitle = t(
'presentBar.isPresentingUser',
'{{displayName}} is presenting',
{ displayName },
);

return (
<ToolbarAvatar
title={presenterTitle}
member={{ userId: state.presenterUserId }}
>
<Box component="span" ml={1} fontSize="medium">
{t('presentBar.isPresenting', 'is presenting')}
</Box>
</ToolbarAvatar>
);
}
9 changes: 6 additions & 3 deletions src/components/common/CollaboratorAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,25 @@ export type CollaboratorAvatarProps = {
displayName?: string;
/** The url of the avatar. */
avatarUrl?: string;
/** The presenter flag. */
presenter?: boolean;
};

export function CollaboratorAvatar({
userId,
displayName,
avatarUrl,
presenter,
}: CollaboratorAvatarProps) {
return (
<ElementAvatar
userId={userId}
displayName={displayName}
avatarUrl={avatarUrl}
sx={(theme) => ({
// We are using a box shadow instead of an outline as it doesn't work
// together with borderRadius in Safari
boxShadow: `0 0 0 2px ${getUserColor(userId)}`,
outline: `${getUserColor(userId)} ${
presenter ? 'dashed' : 'solid'
} 2px`,
border: `2px solid ${theme.palette.background.default}`,
})}
/>
Expand Down
Loading

0 comments on commit a2fdeb4

Please sign in to comment.