Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cnwankwo/together mode stream impl #5391

Draft
wants to merge 67 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
43a8827
Change files
cn0151 Oct 25, 2024
0280dd8
Included a common util method that set call features renderer info
cn0151 Oct 25, 2024
92f7122
Merge branch 'main' into cnwankwo/CallFeatureStream
cn0151 Oct 25, 2024
984e927
Merge branch 'main' into cnwankwo/CallFeatureStream
cn0151 Oct 29, 2024
37710d7
Merge branch 'main' into cnwankwo/CallFeatureStream
cn0151 Oct 31, 2024
4abe027
Addressed comments
cn0151 Oct 31, 2024
592cff1
Together Mode APIs
cn0151 Nov 1, 2024
aa77cda
Update @azure-communication-react-c495d5e5-3d35-4714-a329-a839d316e76…
cn0151 Nov 1, 2024
2a3a676
TogetherModeTypes interface update
cn0151 Nov 3, 2024
a647145
Merge branch 'cnwankwo/together_mode_api' of https://github.com/Azure…
cn0151 Nov 3, 2024
e3220c7
Together Mode Stream Implementation
cn0151 Nov 4, 2024
1fece80
Merge branch 'main' of https://github.com/Azure/communication-ui-libr…
cn0151 Nov 9, 2024
8f2a758
Base changes for together Mode stream and reactions
cn0151 Nov 11, 2024
8fd022a
Merge branch 'main' into cnwankwo/TogetherModeStream_Impl
cn0151 Nov 11, 2024
1abe026
API cleanup
cn0151 Nov 12, 2024
1478855
Merge branch 'cnwankwo/TogetherModeStream_Impl' of https://github.com…
cn0151 Nov 12, 2024
52843b5
Updated together mode reactions
cn0151 Nov 12, 2024
32769bc
Merge branch 'main' into cnwankwo/TogetherModeStream_Impl
cn0151 Nov 12, 2024
a4a2ddc
Merge branch 'main' into cnwankwo/TogetherModeStream_Impl
cn0151 Nov 13, 2024
99faf6b
Merge branch 'main' of https://github.com/Azure/communication-ui-libr…
cn0151 Nov 13, 2024
848574b
Merge branch 'main' into cnwankwo/TogetherModeStream_Impl
cn0151 Nov 16, 2024
514f5e0
make together mode button disabled for ACS user if its not started by…
cn0151 Nov 16, 2024
a9e71f3
Merge branch 'main' into cnwankwo/TogetherModeStream_Impl
cn0151 Nov 18, 2024
fb69100
Together mode with raiseHand, spotlight and mute particpant Implement…
cn0151 Nov 27, 2024
e44d697
Merge branch 'main' into cnwankwo/TogetherModeStream_Impl
cn0151 Nov 27, 2024
0abd7d3
clean up imports
cn0151 Dec 2, 2024
3f1c601
Updated API and cleaned up reactions overlay
cn0151 Dec 4, 2024
d4c3c07
Merge branch 'main' into cnwankwo/TogetherModeStream_Impl
cn0151 Dec 4, 2024
2f643e7
Implementation of together mode notification when started or ended
cn0151 Dec 6, 2024
6a3f326
Logic to switch back to default view if together mode is no longer ac…
cn0151 Dec 6, 2024
db77af3
Logic to switch back to default view if together mode is no longer ac…
cn0151 Dec 6, 2024
4dab149
Merge branch 'main' into cnwankwo/TogetherModeStream_Impl
cn0151 Dec 6, 2024
8c1cdc7
Merge branch 'cnwankwo/TogetherModeStream_Impl' of https://github.com…
cn0151 Dec 6, 2024
08cc53f
fixed merge conflicts
cn0151 Dec 6, 2024
03e5eac
Merge branch 'main' into cnwankwo/TogetherModeStream_Impl
cn0151 Dec 6, 2024
ccded9c
Updated API
cn0151 Dec 7, 2024
c10ef0b
Hover effect on together mode
cn0151 Dec 8, 2024
a3a8e28
reaction animation
cn0151 Dec 8, 2024
aaa380b
Display name auto expand
cn0151 Dec 9, 2024
d75e27b
Merge branch 'main' of https://github.com/Azure/communication-ui-libr…
cn0151 Dec 10, 2024
ea3bfd3
cn0151 Dec 18, 2024
c809c0f
12/19 - demo commit
cn0151 Dec 18, 2024
9cfea5c
Merge https://github.com/Azure/communication-ui-library into cnwankwo…
cn0151 Dec 18, 2024
6b58f71
cleanup together mode styles
cn0151 Dec 20, 2024
136e061
Addressed comments
cn0151 Dec 21, 2024
4bd0aa2
Included mobile menu
cn0151 Dec 26, 2024
4423d4f
fix together mode stream swap
cn0151 Dec 30, 2024
9912da4
Merge branch 'main' of https://github.com/Azure/communication-ui-libr…
cn0151 Dec 31, 2024
bb11bc5
updated api
cn0151 Dec 31, 2024
2aaa7e2
Together mode clean up
cn0151 Jan 5, 2025
7b55ec1
Merge branch 'main' into cnwankwo/TogetherModeStream_Impl
cn0151 Jan 7, 2025
f704eca
Merge branch 'main' into cnwankwo/TogetherModeStream_Impl
cn0151 Jan 9, 2025
cd2d6fb
Updated notification strings
cn0151 Jan 9, 2025
7cf834e
Included conditioal compile statements
cn0151 Jan 9, 2025
701be98
Fix signaling status
cn0151 Jan 9, 2025
ed8495a
Merge branch 'main' of https://github.com/Azure/communication-ui-libr…
cn0151 Jan 9, 2025
e6af06b
Reverted change
cn0151 Jan 9, 2025
a9bca93
Updated api file
cn0151 Jan 10, 2025
c5a5b73
Merge branch 'main' of https://github.com/Azure/communication-ui-libr…
cn0151 Jan 14, 2025
478c8ae
Cleanup
cn0151 Jan 14, 2025
2db6ad6
accessibility fix
cn0151 Jan 14, 2025
4e72d3c
Merge branch 'main' into cnwankwo/TogetherModeStream_Impl
cn0151 Jan 16, 2025
01a0fc9
Merge branch 'main' into cnwankwo/TogetherModeStream_Impl
cn0151 Jan 17, 2025
fa77e7e
Addressed comments
cn0151 Jan 17, 2025
91c4b3d
Merge branch 'main' into cnwankwo/TogetherModeStream_Impl
cn0151 Jan 17, 2025
37346a7
Merge branch 'main' into cnwankwo/TogetherModeStream_Impl
cn0151 Jan 20, 2025
6983984
Merge branch 'main' into cnwankwo/TogetherModeStream_Impl
cn0151 Jan 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "prerelease",
"area": "feature",
"workstream": "togetherMode",
"comment": "TogetherMode Stream view implementation",
"packageName": "@azure/communication-react",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,7 @@ export const createDefaultCommonCallingHandlers = memoizeOne(
}

const togetherModeStreams = callState.togetherMode.streams;

if (!togetherModeStreams.mainVideoStream) {
return;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/calling-stateful-client/src/CallContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,9 @@ export class CallContext {
for (const [userId, seatingPosition] of seatingMap.entries()) {
seatingPositions[userId] = seatingPosition;
}
call.togetherMode.seatingPositions = seatingPositions;
if (Object.keys(seatingPositions).length > 0) {
call.togetherMode.seatingPositions = seatingPositions;
}
}
});
}
Expand Down
84 changes: 44 additions & 40 deletions packages/react-components/src/components/TogetherModeOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License.

/* @conditional-compile-remove(together-mode) */
import React, { useMemo, useState, memo } from 'react';
import React, { useMemo, useState, memo, useEffect } from 'react';
/* @conditional-compile-remove(together-mode) */
import {
Reaction,
Expand All @@ -27,6 +27,7 @@ import { _HighContrastAwareIcon } from './HighContrastAwareIcon';
import {
calculateScaledSize,
getTogetherModeParticipantOverlayStyle,
participantStatusTransitionStyle,
REACTION_MAX_TRAVEL_HEIGHT,
REACTION_TRAVEL_HEIGHT,
setTogetherModeSeatPositionStyle,
Expand Down Expand Up @@ -83,13 +84,21 @@ export const TogetherModeOverlay = memo(
[key: string]: TogetherModeParticipantStatus;
}>({});
const [hoveredParticipantID, setHoveredParticipantID] = useState('');
const [tabbedParticipantID, setTabbedParticipantID] = useState('');

// Reset the Tab key tracking on any other key press
const handleKeyUp = (e: React.KeyboardEvent<HTMLDivElement>, participantId: string) => {
if (e.key === 'Tab') {
setTabbedParticipantID(participantId);
}
};

/*
* The useMemo hook is used to calculate the participant status for the Together Mode overlay.
* It updates the togetherModeParticipantStatus state when there's a change in the remoteParticipants, localParticipant,
* raisedHand, spotlight, isMuted, displayName, or hoveredParticipantID.
*/
useMemo(() => {
const updatedParticipantStatus = useMemo(() => {
const allParticipants = [...remoteParticipants, localParticipant];

const participantsWithVideoAvailable = allParticipants.filter(
Expand All @@ -108,7 +117,12 @@ export const TogetherModeOverlay = memo(
isSpotlighted: !!spotlight,
isMuted,
displayName: displayName || locale.strings.videoGallery.displayNamePlaceholder,
showDisplayName: !!(spotlight || raisedHand || hoveredParticipantID === userId),
showDisplayName: !!(
spotlight ||
raisedHand ||
hoveredParticipantID === userId ||
tabbedParticipantID === userId
),
scaledSize: calculateScaledSize(seatingPosition.width, seatingPosition.height),
seatPositionStyle: setTogetherModeSeatPositionStyle(seatingPosition)
};
Expand All @@ -120,68 +134,58 @@ export const TogetherModeOverlay = memo(
(id) => !updatedSignals[id]
);

setTogetherModeParticipantStatus((prevSignals) => {
const newSignals = { ...prevSignals, ...updatedSignals };
const newSignalsLength = Object.keys(newSignals).length;
const newSignals = { ...togetherModeParticipantStatus, ...updatedSignals };

participantsNotInTogetherModeStream.forEach((id) => {
delete newSignals[id];
});
participantsNotInTogetherModeStream.forEach((id) => {
delete newSignals[id];
});

const hasChanges = Object.keys(newSignals).some(
(key) =>
JSON.stringify(newSignals[key]) !== JSON.stringify(prevSignals[key]) ||
newSignalsLength !== Object.keys(prevSignals).length
);
const hasSignalingChange = Object.keys(newSignals).some(
(key) => JSON.stringify(newSignals[key]) !== JSON.stringify(togetherModeParticipantStatus[key])
);

return hasChanges ? newSignals : prevSignals;
});
const updateTogetherModeParticipantStatusState =
hasSignalingChange || Object.keys(newSignals).length !== Object.keys(togetherModeParticipantStatus).length;
return updateTogetherModeParticipantStatusState ? newSignals : togetherModeParticipantStatus;
}, [
remoteParticipants,
localParticipant,
togetherModeParticipantStatus,
togetherModeSeatPositions,
reactionResources,
locale.strings.videoGallery.displayNamePlaceholder,
hoveredParticipantID
hoveredParticipantID,
tabbedParticipantID
]);

/*
* When a larger participant scene switches to a smaller group in Together Mode,
* participant video streams remain available because their video is still active,
* even though they are not visible in the Together Mode stream.
* Therefore, we rely on the updated seating position values to identify who is included in the Together Mode stream.
* The Together mode seat position will only contain seat coordinates of participants who are visible in the Together Mode stream.
*/
useMemo(() => {
const removedVisibleParticipants = Object.keys(togetherModeParticipantStatus).filter(
(participantId) => !togetherModeSeatPositions[participantId]
);
useEffect(() => {
if (hoveredParticipantID && !updatedParticipantStatus[hoveredParticipantID]) {
setHoveredParticipantID('');
}

setTogetherModeParticipantStatus((prevSignals) => {
const newSignals = { ...prevSignals };
removedVisibleParticipants.forEach((participantId) => {
delete newSignals[participantId];
});
if (tabbedParticipantID && !updatedParticipantStatus[tabbedParticipantID]) {
setTabbedParticipantID('');
}

// Trigger a re-render only if changes occurred
const hasChanges = Object.keys(newSignals).length !== Object.keys(prevSignals).length;
return hasChanges ? newSignals : prevSignals;
});
}, [togetherModeParticipantStatus, togetherModeSeatPositions]);
setTogetherModeParticipantStatus(updatedParticipantStatus);
}, [hoveredParticipantID, tabbedParticipantID, updatedParticipantStatus]);

return (
<div style={{ position: 'absolute', width: '100%', height: '100%' }}>
{Object.values(togetherModeParticipantStatus).map(
(participantStatus) =>
(participantStatus, index) =>
participantStatus.id && (
<div
key={participantStatus.id}
style={{
...getTogetherModeParticipantOverlayStyle(participantStatus.seatPositionStyle)
// border: '1px solid yellow'
}}
onMouseEnter={() => setHoveredParticipantID(participantStatus.id)}
onMouseLeave={() => setHoveredParticipantID('')}
onKeyUp={(e) => handleKeyUp(e, participantStatus.id)}
onBlur={() => setTabbedParticipantID('')}
tabIndex={index}
>
<div>
{participantStatus.reaction?.reactionType && (
Expand Down Expand Up @@ -218,7 +222,7 @@ export const TogetherModeOverlay = memo(
)}

{participantStatus.showDisplayName && (
<div>
<div style={{ ...participantStatusTransitionStyle }} tabIndex={index}>
<div
style={{
...togetherModeParticipantStatusContainer(
Expand Down
4 changes: 1 addition & 3 deletions packages/react-components/src/components/VideoGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,6 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => {
localParticipant={localParticipant}
remoteParticipants={remoteParticipants}
reactionResources={reactionResources}
screenShareComponent={screenShareComponent}
containerWidth={containerWidth}
containerHeight={containerHeight}
/>
Expand All @@ -832,7 +831,6 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => {
localParticipant,
remoteParticipants,
reactionResources,
screenShareComponent,
containerWidth,
containerHeight
]
Expand Down Expand Up @@ -903,7 +901,7 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => {
/* @conditional-compile-remove(together-mode) */
// Teams users can switch to Together mode layout only if they have the capability,
// while ACS users can do so only if Together mode is enabled.
if (layout === 'togetherMode' && canSwitchToTogetherModeLayout) {
if (!screenShareComponent && layout === 'togetherMode' && canSwitchToTogetherModeLayout) {
return <TogetherModeLayout {...layoutProps} />;
}
return <DefaultLayout {...layoutProps} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,13 @@
// Licensed under the MIT License.

/* @conditional-compile-remove(together-mode) */
import React, { useMemo, useRef, useState } from 'react';
/* @conditional-compile-remove(together-mode) */
import { useId } from '@fluentui/react-hooks';
import React from 'react';
/* @conditional-compile-remove(together-mode) */
import { _formatString } from '@internal/acs-ui-common';
/* @conditional-compile-remove(together-mode) */
import { LayoutProps } from './Layout';
/* @conditional-compile-remove(together-mode) */
import { LayerHost, mergeStyles, Stack } from '@fluentui/react';
/* @conditional-compile-remove(together-mode) */
import { renderTiles, useOrganizedParticipants } from './utils/videoGalleryLayoutUtils';
/* @conditional-compile-remove(together-mode) */
import { OverflowGallery } from './OverflowGallery';
/* @conditional-compile-remove(together-mode) */
import { rootLayoutStyle } from './styles/DefaultLayout.styles';
/* @conditional-compile-remove(together-mode) */
import { isNarrowWidth, isShortHeight } from '../utils/responsive';
/* @conditional-compile-remove(together-mode) */
import { innerLayoutStyle, layerHostStyle } from './styles/FloatingLocalVideoLayout.styles';
/* @conditional-compile-remove(together-mode) */
import { videoGalleryLayoutGap } from './styles/Layout.styles';
import { Stack } from '@fluentui/react';

/* @conditional-compile-remove(together-mode) */
/**
Expand All @@ -31,112 +17,7 @@ import { videoGalleryLayoutGap } from './styles/Layout.styles';
* https://reactjs.org/docs/react-api.html#reactmemo
*/
export const TogetherModeLayout = (props: LayoutProps): JSX.Element => {
const {
remoteParticipants = [],
dominantSpeakers,
screenShareComponent,
onRenderRemoteParticipant,
styles,
maxRemoteVideoStreams,
parentWidth,
parentHeight,
overflowGalleryPosition = 'horizontalBottom',
pinnedParticipantUserIds = [],
togetherModeStreamComponent
} = props;
const isNarrow = parentWidth ? isNarrowWidth(parentWidth) : false;

const isShort = parentHeight ? isShortHeight(parentHeight) : false;

const [indexesToRender, setIndexesToRender] = useState<number[]>([]);
const childrenPerPage = useRef(4);

const { gridParticipants, overflowGalleryParticipants } = useOrganizedParticipants({
remoteParticipants,
dominantSpeakers,
maxGridParticipants: maxRemoteVideoStreams,
isScreenShareActive: !!screenShareComponent,
maxOverflowGalleryDominantSpeakers: screenShareComponent
? childrenPerPage.current - (pinnedParticipantUserIds.length % childrenPerPage.current)
: childrenPerPage.current,
pinnedParticipantUserIds,
layout: 'floatingLocalVideo'
});
const { gridTiles, overflowGalleryTiles } = renderTiles(
gridParticipants,
onRenderRemoteParticipant,
maxRemoteVideoStreams,
indexesToRender,
overflowGalleryParticipants,
dominantSpeakers
);

const layerHostId = useId('layerhost');
const togetherModeOverFlowGalleryTiles = useMemo(() => {
let newTiles = overflowGalleryTiles;
if (togetherModeStreamComponent) {
if (screenShareComponent) {
newTiles = gridTiles.concat(overflowGalleryTiles);
}
}
return newTiles;
}, [gridTiles, overflowGalleryTiles, screenShareComponent, togetherModeStreamComponent]);

const overflowGallery = useMemo(() => {
if (overflowGalleryTiles.length === 0 && !props.screenShareComponent) {
return null;
}
return (
<OverflowGallery
isShort={isShort}
onFetchTilesToRender={setIndexesToRender}
isNarrow={isNarrow}
shouldFloatLocalVideo={false}
overflowGalleryElements={togetherModeOverFlowGalleryTiles}
horizontalGalleryStyles={styles?.horizontalGallery}
verticalGalleryStyles={styles?.verticalGallery}
overflowGalleryPosition={overflowGalleryPosition}
onChildrenPerPageChange={(n: number) => {
childrenPerPage.current = n;
}}
parentWidth={parentWidth}
/>
);
}, [
overflowGalleryTiles.length,
props.screenShareComponent,
isShort,
isNarrow,
togetherModeOverFlowGalleryTiles,
styles?.horizontalGallery,
styles?.verticalGallery,
overflowGalleryPosition,
parentWidth
]);

return screenShareComponent ? (
<Stack styles={rootLayoutStyle}>
<LayerHost id={layerHostId} className={mergeStyles(layerHostStyle)} />
<Stack
horizontal={overflowGalleryPosition === 'verticalRight'}
styles={innerLayoutStyle}
tokens={videoGalleryLayoutGap}
>
{props.overflowGalleryPosition === 'horizontalTop' ? overflowGallery : <></>}
{screenShareComponent}
{overflowGalleryTrampoline(overflowGallery, props.overflowGalleryPosition)}
</Stack>
</Stack>
) : (
<Stack>{props.togetherModeStreamComponent}</Stack>
);
};

/* @conditional-compile-remove(together-mode) */
const overflowGalleryTrampoline = (
gallery: JSX.Element | null,
galleryPosition?: 'horizontalBottom' | 'verticalRight' | 'horizontalTop'
): JSX.Element | null => {
return galleryPosition !== 'horizontalTop' ? gallery : <></>;
return gallery;
const { togetherModeStreamComponent } = props;
console.log(`TogetherModeLayout: CHUK-1`);
return <Stack>{togetherModeStreamComponent}</Stack>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export const TogetherModeStream = memo(
reactionResources?: ReactionResources;
localParticipant?: VideoGalleryLocalParticipant;
remoteParticipants?: VideoGalleryRemoteParticipant[];
screenShareComponent?: JSX.Element;
containerWidth?: number;
containerHeight?: number;
}): JSX.Element => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ export const renderTiles = (
maxRemoteVideoStreams: number,
indexesToRender: number[],
overflowGalleryParticipants: VideoGalleryParticipant[],
dominantSpeakers?: string[]
dominantSpeakers?: string[],
togetherModeComponent?: JSX.Element
): { gridTiles: JSX.Element[]; overflowGalleryTiles: JSX.Element[] } => {
const _dominantSpeakers = dominantSpeakers ?? [];
let streamsLeftToRender = maxRemoteVideoStreams;
Expand Down Expand Up @@ -212,7 +213,7 @@ export const renderTiles = (
(p.videoStream?.isAvailable && streamsLeftToRender-- > 0)
);
});

togetherModeComponent && overflowGalleryTiles.push(togetherModeComponent);
return { gridTiles, overflowGalleryTiles };
};

Expand Down
Loading
Loading