communication-react-19
Version:
React library for building modern communication user experiences utilizing Azure Communication Services (React 19 compatible fork)
140 lines • 8.76 kB
JavaScript
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Stack, mergeStyles } from '@fluentui/react';
import { videoContainerStyles } from '../styles/VideoTile.styles';
import { getEmojiResource } from './utils/videoGalleryLayoutUtils';
import { generateStartPositionWave, getReactionMovementStyle, getReactionStyleBucket, moveAnimationStyles, opacityAnimationStyles, reactionOverlayStyle, spriteAnimationStyles } from '../styles/ReactionOverlay.style';
import { REACTION_NUMBER_OF_ANIMATION_FRAMES, REACTION_SCREEN_SHARE_ANIMATION_TIME_MS, REACTION_START_DISPLAY_SIZE, getCombinedKey, getReceivedUnixTime } from './utils/reactionUtils';
const MAX_NUMBER_OF_EMOJIS = 50;
const NUMBER_OF_EMOJI_TYPES = 5;
const REACTION_POSITION_ARRAY_SIZE = 55;
/**
* The overlay responsible for rendering multiple reactions all at once in presentation mode
* @internal
*/
export const RemoteContentShareReactionOverlay = React.memo((props) => {
const { reactionResources, localParticipant, remoteParticipants, hostDivHeight, hostDivWidth } = props;
// Reactions that are currently being animated
const [visibleReactions, setVisibleReactions] = useState([]);
// Dictionary of userId to a reaction status. This is used to track the latest received reaction
// per user to avoid animating the same reaction multiple times and to limit the number of
// active reactions of a certain type.
const latestReceivedReaction = useRef({});
// Track the number of active reactions of each type to limit the number of active reactions
// of a certain type.
const activeTypeCount = useRef({
like: 0,
heart: 0,
laugh: 0,
applause: 0,
surprised: 0
});
// Used to track the total number of reactions ever played. This is a helper variable
// to calculate the reaction movement index (i.e. the .left position of the reaction)
const visibleReactionPosition = useRef(new Array(REACTION_POSITION_ARRAY_SIZE).fill(false));
const remoteParticipantReactions = useMemo(() => {
var _a;
return (_a = remoteParticipants === null || remoteParticipants === void 0 ? void 0 : remoteParticipants.map((remoteParticipant) => remoteParticipant.reaction).filter((reaction) => !!reaction)) !== null && _a !== void 0 ? _a : [];
}, [remoteParticipants]);
const findFirstEmptyPosition = () => {
return visibleReactionPosition.current.findIndex((item) => item === false);
};
const updateVisibleReactions = useCallback((reaction, userId) => {
var _a, _b;
const combinedKey = getCombinedKey(userId, reaction.reactionType, reaction.receivedOn);
const alreadyHandled = ((_a = latestReceivedReaction.current[userId]) === null || _a === void 0 ? void 0 : _a.id) === combinedKey;
if (alreadyHandled) {
return;
}
const activeCount = (_b = activeTypeCount.current[reaction.reactionType]) !== null && _b !== void 0 ? _b : 0;
if (activeCount >= MAX_NUMBER_OF_EMOJIS / NUMBER_OF_EMOJI_TYPES) {
latestReceivedReaction.current[userId] = {
id: combinedKey,
status: 'ignored'
};
return;
}
activeTypeCount.current[reaction.reactionType] += 1;
latestReceivedReaction.current[userId] = {
id: combinedKey,
status: 'animating'
};
const reactionMovementIndex = findFirstEmptyPosition();
visibleReactionPosition.current[reactionMovementIndex] = true;
setVisibleReactions([
...visibleReactions,
{
reaction: reaction,
id: combinedKey,
reactionMovementIndex: reactionMovementIndex,
styleBucket: getReactionStyleBucket()
}
]);
return;
}, [activeTypeCount, visibleReactions]);
const removeVisibleReaction = (reactionType, id, index) => {
setVisibleReactions(visibleReactions.filter((reaction) => reaction.id !== id));
visibleReactionPosition.current[index] = false;
activeTypeCount.current[reactionType] -= 1;
Object.entries(latestReceivedReaction.current).forEach(([userId, reaction]) => {
const userLastReaction = latestReceivedReaction.current[userId];
if (reaction.id === id && userLastReaction) {
userLastReaction.status = 'completedAnimating';
}
});
};
// Update visible reactions when local participant sends a reaction
useEffect(() => {
if (localParticipant === null || localParticipant === void 0 ? void 0 : localParticipant.reaction) {
updateVisibleReactions(localParticipant.reaction, localParticipant.userId);
}
}, [localParticipant, updateVisibleReactions]);
// Update visible reactions when remote participants send a reaction
useEffect(() => {
remoteParticipants === null || remoteParticipants === void 0 ? void 0 : remoteParticipants.map((participant) => {
if (participant === null || participant === void 0 ? void 0 : participant.reaction) {
updateVisibleReactions(participant.reaction, participant.userId);
}
});
}, [remoteParticipantReactions, remoteParticipants, updateVisibleReactions]);
// Note: canRenderReaction shouldn't be needed as we remove the animation on the onAnimationEnd event
const canRenderReaction = (reaction, id, movementIndex) => {
// compare current time to reaction.received at and see if more than 4 seconds has elapsed
const canRender = Date.now() - getReceivedUnixTime(reaction.receivedOn) < REACTION_SCREEN_SHARE_ANIMATION_TIME_MS;
// Clean up the reaction if it's not in the visible reaction list
if (!canRender) {
removeVisibleReaction(reaction === null || reaction === void 0 ? void 0 : reaction.reactionType, id, movementIndex);
}
return canRender;
};
const styleBucket = () => getReactionStyleBucket();
const displaySizePx = () => REACTION_START_DISPLAY_SIZE * styleBucket().sizeScale;
const containerHeight = hostDivHeight !== null && hostDivHeight !== void 0 ? hostDivHeight : 0;
const containerWidth = (hostDivWidth !== null && hostDivWidth !== void 0 ? hostDivWidth : 0) - displaySizePx();
const leftPosition = (position) => generateStartPositionWave(position, containerWidth / 2, true);
const reactionMovementStyle = (position) => getReactionMovementStyle(leftPosition(position));
return (React.createElement(Stack, { className: mergeStyles(videoContainerStyles, {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'transparent'
}) }, visibleReactions.map((reaction) => {
var _a;
return (React.createElement("div", { key: reaction.id, style: reactionOverlayStyle },
React.createElement("div", { className: "reaction-item" }, canRenderReaction(reaction.reaction, reaction.id, reaction.reactionMovementIndex) && (
// First div - Section that fixes the travel height and applies the movement animation
// Second div - Keeps track of active sprites and responsible for marking, counting
// and removing reactions. Responsible for opacity controls as the sprite emoji animates
// Third div - Responsible for calculating the point of X axis where the reaction will start animation
// Fourth div - Play Animation as the other animation applies on the base play animation for the sprite
React.createElement("div", { style: moveAnimationStyles(containerHeight / 2, // dividing by two because reactionOverlayStyle height is set to 50%
(containerHeight / 2) * (1 - reaction.styleBucket.heightMaxScale)) },
React.createElement("div", { onAnimationEnd: () => {
removeVisibleReaction(reaction.reaction.reactionType, reaction.id, reaction.reactionMovementIndex);
}, style: opacityAnimationStyles(reaction.styleBucket.opacityMax) },
React.createElement("div", { style: reactionMovementStyle(reaction.reactionMovementIndex) },
React.createElement("div", { style: spriteAnimationStyles(REACTION_NUMBER_OF_ANIMATION_FRAMES, displaySizePx(), (_a = getEmojiResource(reaction === null || reaction === void 0 ? void 0 : reaction.reaction.reactionType, reactionResources)) !== null && _a !== void 0 ? _a : '') }))))))));
})));
});
//# sourceMappingURL=RemoteContentShareReactionOverlay.js.map