Skip to content

Commit

Permalink
feat: Shorter participant actions list in demo mode
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverlaz committed Dec 22, 2023
1 parent 6f67621 commit 264553c
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import { WithTooltip } from '../Tooltip';
import { Icon } from '../Icon';
import { Avatar } from '../Avatar';
import { useParticipantViewContext } from '../../core';

type CallParticipantListingItemProps = {
/** Participant object be rendered */
Expand Down Expand Up @@ -84,7 +85,7 @@ export const CallParticipantListingItem = ({
)}

<MenuToggle placement="bottom-end" ToggleButton={ToggleButton}>
<ParticipantActionsContextMenu participant={participant} />
<ParticipantActionsContextMenu />
</MenuToggle>
</div>
</div>
Expand Down Expand Up @@ -131,15 +132,9 @@ const ToggleButton = forwardRef<HTMLButtonElement, ToggleMenuButtonProps>(
},
);

export const ParticipantActionsContextMenu = ({
participant,
participantViewElement,
videoElement,
}: {
participant: StreamVideoParticipant;
participantViewElement?: HTMLDivElement | null;
videoElement?: HTMLVideoElement | null;
}) => {
export const ParticipantActionsContextMenu = () => {
const { participant, participantViewElement, videoElement } =
useParticipantViewContext();
const [fullscreenModeOn, setFullscreenModeOn] = useState(
!!document.fullscreenElement,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { forwardRef } from 'react';
import { ComponentType, forwardRef } from 'react';
import { Placement } from '@floating-ui/react';
import { SfuModels } from '@stream-io/video-client';
import { useCall, useI18n } from '@stream-io/video-react-bindings';
Expand All @@ -9,7 +9,7 @@ import {
IconButton,
MenuToggle,
Notification,
ParticipantActionsContextMenu,
ParticipantActionsContextMenu as DefaultParticipantActionsContextMenu,
ToggleMenuButtonProps,
} from '../../../components';
import { Reaction } from '../../../components/Reaction';
Expand All @@ -28,6 +28,10 @@ export type DefaultParticipantViewUIProps = {
* Option to show/hide menu button component
*/
showMenuButton?: boolean;
/**
* Custom component to render the context menu
*/
ParticipantActionsContextMenu?: ComponentType;
};

const ToggleButton = forwardRef<HTMLButtonElement, ToggleMenuButtonProps>(
Expand Down Expand Up @@ -64,9 +68,9 @@ export const DefaultParticipantViewUI = ({
indicatorsVisible = true,
menuPlacement = 'bottom-start',
showMenuButton = true,
ParticipantActionsContextMenu = DefaultParticipantActionsContextMenu,
}: DefaultParticipantViewUIProps) => {
const { participant, participantViewElement, trackType, videoElement } =
useParticipantViewContext();
const { participant, trackType } = useParticipantViewContext();
const { publishedTracks } = participant;

const hasScreenShare = publishedTracks.includes(
Expand Down Expand Up @@ -94,11 +98,7 @@ export const DefaultParticipantViewUI = ({
placement={menuPlacement}
ToggleButton={ToggleButton}
>
<ParticipantActionsContextMenu
participantViewElement={participantViewElement}
participant={participant}
videoElement={videoElement}
/>
<ParticipantActionsContextMenu />
</MenuToggle>
)}
<Reaction participant={participant} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,44 @@
import { useEffect, useState } from 'react';
import {
DefaultParticipantViewUI,
GenericMenu,
GenericMenuButtonItem,
Icon,
OwnCapability,
ParticipantActionsContextMenu,
Restricted,
SfuModels,
useCall,
useI18n,
useParticipantViewContext,
} from '@stream-io/video-react-sdk';
import { DebugStatsView } from './DebugStatsView';
import { useIsDebugMode } from './useIsDebugMode';
import { useIsDemoEnvironment } from '../../context/AppEnvironmentContext';

export const DebugParticipantViewUI = () => {
const call = useCall();
const { participant } = useParticipantViewContext();
const { sessionId, userId, videoStream } = participant;

const isDemoEnvironment = useIsDemoEnvironment();
const participantContextMenuActions = isDemoEnvironment
? CustomParticipantActionsContextMenu
: ParticipantActionsContextMenu;

const isDebug = useIsDebugMode();
if (!isDebug) return <DefaultParticipantViewUI />;
if (!isDebug) {
return (
<DefaultParticipantViewUI
ParticipantActionsContextMenu={participantContextMenuActions}
/>
);
}
return (
<>
<DefaultParticipantViewUI />
<DefaultParticipantViewUI
ParticipantActionsContextMenu={participantContextMenuActions}
/>
<div className="rd__debug__extra">
<DebugStatsView
call={call!}
Expand All @@ -26,3 +50,194 @@ export const DebugParticipantViewUI = () => {
</>
);
};

const CustomParticipantActionsContextMenu = () => {
const call = useCall();
const { t } = useI18n();

const { participant, participantViewElement, videoElement } =
useParticipantViewContext();
const { pin, publishedTracks, sessionId, userId } = participant;

const hasAudio = publishedTracks.includes(SfuModels.TrackType.AUDIO);
const hasVideo = publishedTracks.includes(SfuModels.TrackType.VIDEO);
const hasScreenShare = publishedTracks.includes(
SfuModels.TrackType.SCREEN_SHARE,
);
const hasScreenShareAudio = publishedTracks.includes(
SfuModels.TrackType.SCREEN_SHARE_AUDIO,
);

const [fullscreenModeOn, setFullscreenModeOn] = useState(
!!document.fullscreenElement,
);
const [pictureInPictureElement, setPictureInPictureElement] = useState(
document.pictureInPictureElement,
);

const blockUser = () => call?.blockUser(userId);
const muteAudio = () => call?.muteUser(userId, 'audio');
const muteVideo = () => call?.muteUser(userId, 'video');
const muteScreenShare = () => call?.muteUser(userId, 'screenshare');
const muteScreenShareAudio = () =>
call?.muteUser(userId, 'screenshare_audio');

const toggleParticipantPinnedAt = () => {
if (pin) {
call?.unpin(sessionId);
} else {
call?.pin(sessionId);
}
};

const pinForEveryone = () => {
call
?.pinForEveryone({
user_id: userId,
session_id: sessionId,
})
.catch((err) => {
console.error(`Failed to pin participant ${userId}`, err);
});
};

const unpinForEveryone = () => {
call
?.unpinForEveryone({
user_id: userId,
session_id: sessionId,
})
.catch((err) => {
console.error(`Failed to unpin participant ${userId}`, err);
});
};

const toggleFullscreenMode = () => {
if (!fullscreenModeOn) {
return participantViewElement
?.requestFullscreen()
.then(() => setFullscreenModeOn(true))
.catch(console.error);
}

document
.exitFullscreen()
.catch(console.error)
.finally(() => setFullscreenModeOn(false));
};

useEffect(() => {
if (!videoElement) return;

const handlePictureInPicture = () => {
setPictureInPictureElement(document.pictureInPictureElement);
};

videoElement.addEventListener(
'enterpictureinpicture',
handlePictureInPicture,
);
videoElement.addEventListener(
'leavepictureinpicture',
handlePictureInPicture,
);

return () => {
videoElement.removeEventListener(
'enterpictureinpicture',
handlePictureInPicture,
);
videoElement.removeEventListener(
'leavepictureinpicture',
handlePictureInPicture,
);
};
}, [videoElement]);

const togglePictureInPicture = () => {
if (videoElement && pictureInPictureElement !== videoElement) {
return videoElement
.requestPictureInPicture()
.catch(console.error) as Promise<void>;
}

document.exitPictureInPicture().catch(console.error);
};

return (
<GenericMenu>
<GenericMenuButtonItem
onClick={toggleParticipantPinnedAt}
disabled={pin && !pin.isLocalPin}
>
<Icon icon="pin" />
{pin ? t('Unpin') : t('Pin')}
</GenericMenuButtonItem>
<Restricted requiredGrants={[OwnCapability.PIN_FOR_EVERYONE]}>
<GenericMenuButtonItem
onClick={pinForEveryone}
disabled={pin && !pin.isLocalPin}
>
<Icon icon="pin" />
{t('Pin for everyone')}
</GenericMenuButtonItem>
<GenericMenuButtonItem
onClick={unpinForEveryone}
disabled={!pin || pin.isLocalPin}
>
<Icon icon="pin" />
{t('Unpin for everyone')}
</GenericMenuButtonItem>
</Restricted>
<Restricted requiredGrants={[OwnCapability.BLOCK_USERS]}>
<GenericMenuButtonItem onClick={blockUser}>
<Icon icon="not-allowed" />
{t('Block')}
</GenericMenuButtonItem>
</Restricted>
<Restricted requiredGrants={[OwnCapability.MUTE_USERS]}>
{hasVideo && (
<GenericMenuButtonItem onClick={muteVideo}>
<Icon icon="camera-off-outline" />
{t('Turn off video')}
</GenericMenuButtonItem>
)}
{hasScreenShare && (
<GenericMenuButtonItem onClick={muteScreenShare}>
<Icon icon="screen-share-off" />
{t('Turn off screen share')}
</GenericMenuButtonItem>
)}
{hasAudio && (
<GenericMenuButtonItem onClick={muteAudio}>
<Icon icon="no-audio" />
{t('Mute audio')}
</GenericMenuButtonItem>
)}
{hasScreenShareAudio && (
<GenericMenuButtonItem onClick={muteScreenShareAudio}>
<Icon icon="no-audio" />
{t('Mute screen share audio')}
</GenericMenuButtonItem>
)}
</Restricted>
{participantViewElement && (
<GenericMenuButtonItem onClick={toggleFullscreenMode}>
{t('{{ direction }} fullscreen', {
direction: fullscreenModeOn ? t('Leave') : t('Enter'),
})}
</GenericMenuButtonItem>
)}
{videoElement && document.pictureInPictureEnabled && (
<GenericMenuButtonItem onClick={togglePictureInPicture}>
{t('{{ direction }} picture-in-picture', {
direction:
pictureInPictureElement === videoElement
? t('Leave')
: t('Enter'),
})}
</GenericMenuButtonItem>
)}
</GenericMenu>
);
};

0 comments on commit 264553c

Please sign in to comment.