UNPKG

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
// 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