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

Enable together mode for mobile #5581

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
147 changes: 75 additions & 72 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,55 +134,41 @@ 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%' }}>
Expand All @@ -182,43 +182,13 @@ export const TogetherModeOverlay = memo(
}}
onMouseEnter={() => setHoveredParticipantID(participantStatus.id)}
onMouseLeave={() => setHoveredParticipantID('')}
onKeyUp={(e) => handleKeyUp(e, participantStatus.id)}
onBlur={() => setTabbedParticipantID('')}
tabIndex={0}
>
<div>
{participantStatus.reaction?.reactionType && (
// First div - Section that fixes the travel height and applies the movement animation
// Second div - Responsible for ensuring the sprite emoji is always centered in the participant seat position
// Third div - Play Animation as the other animation applies on the base play animation for the sprite
<div
style={moveAnimationStyles(
parseFloat(participantStatus.seatPositionStyle.seatPosition.height) *
REACTION_MAX_TRAVEL_HEIGHT,
parseFloat(participantStatus.seatPositionStyle.seatPosition.height) * REACTION_TRAVEL_HEIGHT
)}
>
<div
style={{
...togetherModeParticipantEmojiSpriteStyle(
emojiSize,
participantStatus.scaledSize || 1,
participantStatus.seatPositionStyle.seatPosition.width
)
}}
>
<div
style={spriteAnimationStyles(
REACTION_NUMBER_OF_ANIMATION_FRAMES,
participantStatus.scaledSize || 1,
(participantStatus.reaction &&
getEmojiResource(participantStatus?.reaction.reactionType, reactionResources)) ??
''
)}
/>
</div>
</div>
)}

{participantStatus.showDisplayName && (
<div>
<div style={{ ...participantStatusTransitionStyle }} tabIndex={0}>
<div
style={{
...togetherModeParticipantStatusContainer(
Expand Down Expand Up @@ -254,6 +224,39 @@ export const TogetherModeOverlay = memo(
</div>
</div>
)}

{participantStatus.reaction?.reactionType && (
// First div - Section that fixes the travel height and applies the movement animation
// Second div - Responsible for ensuring the sprite emoji is always centered in the participant seat position
// Third div - Play Animation as the other animation applies on the base play animation for the sprite
<div
style={moveAnimationStyles(
parseFloat(participantStatus.seatPositionStyle.seatPosition.height) *
REACTION_MAX_TRAVEL_HEIGHT,
parseFloat(participantStatus.seatPositionStyle.seatPosition.height) * REACTION_TRAVEL_HEIGHT
)}
>
<div
style={{
...togetherModeParticipantEmojiSpriteStyle(
emojiSize,
participantStatus.scaledSize || 1,
participantStatus.seatPositionStyle.seatPosition.width
)
}}
>
<div
style={spriteAnimationStyles(
REACTION_NUMBER_OF_ANIMATION_FRAMES,
participantStatus.scaledSize || 1,
(participantStatus.reaction &&
getEmojiResource(participantStatus?.reaction.reactionType, reactionResources)) ??
''
)}
/>
</div>
</div>
)}
</div>
</div>
)
Expand Down
18 changes: 7 additions & 11 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 All @@ -859,9 +857,7 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => {
pinnedParticipantUserIds: pinnedParticipants,
overflowGalleryPosition,
localVideoTileSize,
spotlightedParticipantUserIds: spotlightedParticipants,
/* @conditional-compile-remove(together-mode) */
togetherModeStreamComponent
spotlightedParticipantUserIds: spotlightedParticipants
}),
[
remoteParticipants,
Expand All @@ -879,9 +875,7 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => {
pinnedParticipants,
overflowGalleryPosition,
localVideoTileSize,
spotlightedParticipants,
/* @conditional-compile-remove(together-mode) */
togetherModeStreamComponent
spotlightedParticipants
]
);

Expand All @@ -903,15 +897,17 @@ 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) {
return <TogetherModeLayout {...layoutProps} />;
if (!screenShareComponent && layout === 'togetherMode' && canSwitchToTogetherModeLayout) {
return <TogetherModeLayout togetherModeStreamComponent={togetherModeStreamComponent} />;
}
return <DefaultLayout {...layoutProps} />;
}, [
/* @conditional-compile-remove(together-mode) */ canSwitchToTogetherModeLayout,
layout,
layoutProps,
screenShareParticipant
screenShareComponent,
screenShareParticipant,
/* @conditional-compile-remove(together-mode) */ togetherModeStreamComponent
]);

return (
Expand Down
Loading
Loading