diff --git a/packages/storybook8/stories/Components/ParticipantList/Docs.mdx b/packages/storybook8/stories/Components/ParticipantList/Docs.mdx new file mode 100644 index 00000000000..8be58fb5d62 --- /dev/null +++ b/packages/storybook8/stories/Components/ParticipantList/Docs.mdx @@ -0,0 +1,46 @@ +import { ParticipantList } from '@azure/communication-react'; +import { Canvas, Meta } from '@storybook/blocks'; +import * as ParticipantStories from './index.stories'; + +import DefaultCallParticipantListExampleText from '!!raw-loader!./snippets/DefaultCall.snippet.tsx'; +import DefaultChatParticipantListExampleText from '!!raw-loader!./snippets/DefaultChat.snippet.tsx'; +import InteractiveCallParticipantListExampleText from '!!raw-loader!./snippets/InteractiveCall.snippet.tsx'; +import ParticipantListWithExcludedUserExampleText from '!!raw-loader!./snippets/WithExcludedUser.snippet.tsx'; + + + +# ParticipantList + +ParticipantList renders a list of all calling or chat participants. + +## Default example for Chat + +The ParticipantList for chat is by default a list of [ParticipantItem](./?path=/docs/ui-components-participantitem--participant-item) components linked with state around each chat participant. + + + +## Default example for Calling + +ParticipantList for calling is by default a list of [ParticipantItem](./?path=/docs/ui-components-participantitem--participant-item) components with presence linked to the participant call state, as well as icons for microphone and screen sharing states. + + + +## ParticipantList with local user excluded from the list + +Local user can be excluded from the participant list as shown in the example below. + + + +## Interactive Call example + +ParticipantList is designed with a rendering override, `onRenderParticipant`, which allows you to have your own design or use your own [ParticipantItem](./?path=/docs/ui-components-participantitem--participant-item) components with their context menu style enabling interaction with this participant. For example, you can add menu items and icons to the participants using `menuItems` and `onRenderIcon` properties of [ParticipantItem](./?path=/docs/ui-components-participantitem--participant-item#props) like in the code below. + +For simplicity, React `useState` is used to keep the state of every participant to decide which menu items and icons to show. You can now mute and unmute by clicking a participant in the rendered example below. + +Note: Each `ParticipantItem` needs a unique key to avoid warnings for children in a list. + + + +## Props + +tbd.. diff --git a/packages/storybook8/stories/Components/ParticipantList/ParticipantList.story.tsx b/packages/storybook8/stories/Components/ParticipantList/ParticipantList.story.tsx new file mode 100644 index 00000000000..2c767983e6e --- /dev/null +++ b/packages/storybook8/stories/Components/ParticipantList/ParticipantList.story.tsx @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ParticipantList as ParticipantListComponent, ParticipantListParticipant } from '@azure/communication-react'; +import { Stack } from '@fluentui/react'; +import React from 'react'; + +const ParticipantListStory: (args) => JSX.Element = (args) => { + const participantsControls = [...args.remoteParticipants, ...args.localParticipant]; + + const mockParticipants: ParticipantListParticipant[] = participantsControls.map((p, i) => { + return { + userId: `userId ${i}`, + displayName: p.name, + state: p.status, + isMuted: p.isMuted, + isScreenSharing: p.isScreenSharing, + isRemovable: true + }; + }); + + const myUserId = mockParticipants[mockParticipants.length - 1].userId; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const onParticipantRemove = (_userId: string): void => { + // Do something when remove a participant from list + }; + + return ( + + + + ); +}; + +export const ParticipantList = ParticipantListStory.bind({}); diff --git a/packages/storybook8/stories/Components/ParticipantList/index.stories.tsx b/packages/storybook8/stories/Components/ParticipantList/index.stories.tsx new file mode 100644 index 00000000000..2ff9c41f39e --- /dev/null +++ b/packages/storybook8/stories/Components/ParticipantList/index.stories.tsx @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ParticipantList as ParticipantListComponent } from '@azure/communication-react'; +import { Meta } from '@storybook/react'; +import { controlsToAdd, defaultLocalParticipant, defaultRemoteParticipants, hiddenControl } from '../../controlsUtils'; +import { DefaultCallParticipantListExample } from './snippets/DefaultCall.snippet'; +import { DefaultChatParticipantListExample } from './snippets/DefaultChat.snippet'; +import { InteractiveCallParticipantListExample } from './snippets/InteractiveCall.snippet'; +import { ParticipantListWithExcludedUserExample } from './snippets/WithExcludedUser.snippet'; +export { ParticipantList } from './ParticipantList.story'; + +export const DefaultCallParticipantListDocsOnly = { + render: DefaultCallParticipantListExample +}; + +export const DefaultChatParticipantListDocsOnly = { + render: DefaultChatParticipantListExample +}; + +export const InteractiveCallParticipantListDocsOnly = { + render: InteractiveCallParticipantListExample +}; + +export const ParticipantListWithExcludedUserDocsOnly = { + render: ParticipantListWithExcludedUserExample +}; + +const meta: Meta = { + title: 'Components/Participant List', + component: ParticipantListComponent, + argTypes: { + excludeMe: controlsToAdd.excludeMeFromList, + localParticipant: controlsToAdd.localParticipant, + remoteParticipants: controlsToAdd.remoteParticipants, + // Hiding auto-generated controls + participants: hiddenControl, + myUserId: hiddenControl, + onRenderParticipant: hiddenControl, + onRenderAvatar: hiddenControl, + onParticipantRemove: hiddenControl, + onRemoveParticipant: hiddenControl, + onFetchParticipantMenuItems: hiddenControl, + styles: hiddenControl + }, + args: { + excludeMe: false, + localParticipant: defaultLocalParticipant, + remoteParticipants: defaultRemoteParticipants + } +} as Meta; + +export default meta; diff --git a/packages/storybook8/stories/Components/ParticipantList/snippets/DefaultCall.snippet.tsx b/packages/storybook8/stories/Components/ParticipantList/snippets/DefaultCall.snippet.tsx new file mode 100644 index 00000000000..4c331fad224 --- /dev/null +++ b/packages/storybook8/stories/Components/ParticipantList/snippets/DefaultCall.snippet.tsx @@ -0,0 +1,50 @@ +import { CallParticipantListParticipant, FluentThemeProvider, ParticipantList } from '@azure/communication-react'; +import { Stack } from '@fluentui/react'; +import React from 'react'; + +export const DefaultCallParticipantListExample: () => JSX.Element = () => { + const mockParticipants: CallParticipantListParticipant[] = [ + { + userId: 'user1', + displayName: 'You', + state: 'Connected', + isMuted: true, + isScreenSharing: false, + isRemovable: true + }, + { + userId: 'user2', + displayName: 'Hal Jordan', + state: 'Connected', + isMuted: true, + isScreenSharing: true, + isRemovable: true + }, + { + userId: 'user3', + displayName: 'Barry Allen', + state: 'Idle', + isMuted: false, + isScreenSharing: false, + isRemovable: true, + raisedHand: { raisedHandOrderPosition: 1 } + }, + { + userId: 'user4', + displayName: 'Bruce Wayne', + state: 'Connecting', + isMuted: false, + isScreenSharing: false, + isRemovable: false + } + ]; + + return ( + + +
Participants
+ +
+
+ ); +}; diff --git a/packages/storybook8/stories/Components/ParticipantList/snippets/DefaultChat.snippet.tsx b/packages/storybook8/stories/Components/ParticipantList/snippets/DefaultChat.snippet.tsx new file mode 100644 index 00000000000..2c8999bf4f0 --- /dev/null +++ b/packages/storybook8/stories/Components/ParticipantList/snippets/DefaultChat.snippet.tsx @@ -0,0 +1,35 @@ +import { ParticipantListParticipant, ParticipantList } from '@azure/communication-react'; +import { Stack } from '@fluentui/react'; +import React from 'react'; + +export const DefaultChatParticipantListExample: () => JSX.Element = () => { + const mockParticipants: ParticipantListParticipant[] = [ + { + userId: 'user 1', + displayName: 'You', + isRemovable: true + }, + { + userId: 'user 2', + displayName: 'Hal Jordan', + isRemovable: true + }, + { + userId: 'user 3', + displayName: 'Barry Allen', + isRemovable: true + }, + { + userId: 'user 4', + displayName: 'Bruce Wayne', + isRemovable: true + } + ]; + + return ( + +
Participants
+ +
+ ); +}; diff --git a/packages/storybook8/stories/Components/ParticipantList/snippets/InteractiveCall.snippet.tsx b/packages/storybook8/stories/Components/ParticipantList/snippets/InteractiveCall.snippet.tsx new file mode 100644 index 00000000000..cb8d3540fa4 --- /dev/null +++ b/packages/storybook8/stories/Components/ParticipantList/snippets/InteractiveCall.snippet.tsx @@ -0,0 +1,103 @@ +import { + CallParticipantListParticipant, + FluentThemeProvider, + ParticipantList, + ParticipantItem, + ParticipantListParticipant +} from '@azure/communication-react'; +import { Icon, IContextualMenuItem, PersonaPresence, Stack } from '@fluentui/react'; +import React, { useState } from 'react'; + +const mockParticipants: CallParticipantListParticipant[] = [ + { + userId: 'user1', + displayName: 'You', + state: 'Connected', + isMuted: true, + isRemovable: true + }, + { + userId: 'user2', + displayName: 'Peter Parker', + state: 'Connected', + isMuted: false, + isRemovable: true + }, + { + userId: 'user3', + displayName: 'Matthew Murdock', + state: 'Idle', + isMuted: false, + isRemovable: true + }, + { + userId: 'user4', + displayName: 'Frank Castiglione', + state: 'Connecting', + isMuted: false, + isRemovable: false + } +]; + +export const InteractiveCallParticipantListExample: () => JSX.Element = () => { + const [participants, setParticpants] = useState(mockParticipants); + + const mockMyUserId = 'user1'; + + const onRenderParticipant = (participant: ParticipantListParticipant): JSX.Element => { + const participantIndex = participants.map((p) => p.userId).indexOf(participant.userId); + + const callingParticipant = participants[participantIndex] as CallParticipantListParticipant; + + let presence: PersonaPresence | undefined = undefined; + if (callingParticipant) { + if (callingParticipant.state === 'Connected') { + presence = PersonaPresence.online; + } else if (callingParticipant.state === 'Idle') { + presence = PersonaPresence.away; + } else if (callingParticipant.state === 'Connecting') { + presence = PersonaPresence.offline; + } + } + + const menuItems: IContextualMenuItem[] = [ + { + key: 'mute', + text: callingParticipant.isMuted ? 'Unmute' : 'Mute', + onClick: () => { + const newParticipants = [...participants]; + newParticipants[participantIndex].isMuted = !participants[participantIndex].isMuted; + setParticpants(newParticipants); + } + } + ]; + + const onRenderIcon = callingParticipant?.isMuted ? () => : () => <>; + + if (participant.displayName) { + return ( + + ); + } + return <>; + }; + + return ( + + +
Participants
+ +
+
+ ); +}; diff --git a/packages/storybook8/stories/Components/ParticipantList/snippets/WithExcludedUser.snippet.tsx b/packages/storybook8/stories/Components/ParticipantList/snippets/WithExcludedUser.snippet.tsx new file mode 100644 index 00000000000..bf328e6ed36 --- /dev/null +++ b/packages/storybook8/stories/Components/ParticipantList/snippets/WithExcludedUser.snippet.tsx @@ -0,0 +1,49 @@ +import { CallParticipantListParticipant, FluentThemeProvider, ParticipantList } from '@azure/communication-react'; +import { Stack } from '@fluentui/react'; +import React from 'react'; + +const mockParticipants: CallParticipantListParticipant[] = [ + { + userId: 'user1', + displayName: 'You', + state: 'Connected', + isMuted: true, + isScreenSharing: false, + isRemovable: true + }, + { + userId: 'user2', + displayName: 'Hal Jordan', + state: 'Connected', + isMuted: true, + isScreenSharing: true, + isRemovable: true + }, + { + userId: 'user3', + displayName: 'Barry Allen', + state: 'Idle', + isMuted: false, + isScreenSharing: false, + isRemovable: true + }, + { + userId: 'user4', + displayName: 'Bruce Wayne', + state: 'Connecting', + isMuted: false, + isScreenSharing: false, + isRemovable: false + } +]; + +export const ParticipantListWithExcludedUserExample: () => JSX.Element = () => { + return ( + + +
Participants
+ +
+
+ ); +}; diff --git a/packages/storybook8/stories/controlsUtils.ts b/packages/storybook8/stories/controlsUtils.ts index e4bf45aa1d6..0e3cd44ce70 100644 --- a/packages/storybook8/stories/controlsUtils.ts +++ b/packages/storybook8/stories/controlsUtils.ts @@ -73,8 +73,8 @@ const defaultControlsGridParticipants = [ } ]; -const defaultLocalParticipant = [{ name: 'You', status: 'Connected', isMuted: false, isScreenSharing: false }]; -const defaultRemoteParticipants = [ +export const defaultLocalParticipant = [{ name: 'You', status: 'Connected', isMuted: false, isScreenSharing: false }]; +export const defaultRemoteParticipants = [ { name: 'Rick', status: 'InLobby', isMuted: false, isScreenSharing: false }, { name: 'Daryl', status: 'Connecting', isMuted: false, isScreenSharing: false }, { name: 'Michonne', status: 'Idle', isMuted: false, isScreenSharing: false } @@ -298,7 +298,7 @@ export const controlsToAdd = { name: 'Width (px)' }, localParticipantDisplayName: { control: 'text', defaultValue: 'John Doe', name: 'Local Participant displayName' }, - localParticipant: { control: 'object', defaultValue: defaultLocalParticipant, name: 'Your information' }, + localParticipant: { control: 'object', name: 'Your information' }, localVideoInverted: { control: 'boolean', defaultValue: true, name: 'Invert Local Video' }, localVideoStreamEnabled: { control: 'boolean', defaultValue: true, name: 'Turn Local Video On' }, messageDeliveredTooltipText: { control: 'text', defaultValue: 'Sent', name: 'Delivered icon tooltip text' }, @@ -335,7 +335,7 @@ export const controlsToAdd = { 'Rick, Daryl, Michonne, Dwight, Pam, Michael, Jim, Kevin, Creed, Angela, Andy, Stanley, Meredith, Phyllis, Oscar, Ryan, Kelly, Andy, Toby, Darryl, Gabe, Erin', name: 'Remote participants (comma separated)' }, - remoteParticipants: { control: 'object', defaultValue: defaultRemoteParticipants, name: 'Remote participants' }, + remoteParticipants: { control: 'object', name: 'Remote participants' }, requiredDisplayName: { control: 'text', defaultValue: 'John Smith',