Animate call link join requests
This commit is contained in:
parent
7e9773a144
commit
e51cde1770
5 changed files with 117 additions and 15 deletions
|
@ -14,10 +14,13 @@
|
||||||
width: 364px;
|
width: 364px;
|
||||||
padding-inline: 0;
|
padding-inline: 0;
|
||||||
padding-block-start: 0;
|
padding-block-start: 0;
|
||||||
background: $color-gray-78;
|
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.CallingPendingParticipants--Expandable {
|
||||||
|
background: $color-gray-78;
|
||||||
|
}
|
||||||
|
|
||||||
.CallingPendingParticipants--Expanded {
|
.CallingPendingParticipants--Expanded {
|
||||||
padding-block-end: 2px;
|
padding-block-end: 2px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -858,7 +858,7 @@ export function CallScreen({
|
||||||
renderRaisedHandsToast={renderRaisedHandsToast}
|
renderRaisedHandsToast={renderRaisedHandsToast}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
{pendingParticipants.length ? (
|
{isCallLinkAdmin ? (
|
||||||
<CallingPendingParticipants
|
<CallingPendingParticipants
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
participants={pendingParticipants}
|
participants={pendingParticipants}
|
||||||
|
|
|
@ -60,6 +60,26 @@ export function Many(): JSX.Element {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Changing(): JSX.Element {
|
||||||
|
const counts = [0, 1, 2, 3, 2, 1];
|
||||||
|
const [countIndex, setCountIndex] = React.useState<number>(0);
|
||||||
|
React.useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setCountIndex((countIndex + 1) % counts.length);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [countIndex, counts.length]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CallingPendingParticipants
|
||||||
|
{...createProps({
|
||||||
|
participants: allRemoteParticipants.slice(0, counts[countIndex]),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ExpandedOne(): JSX.Element {
|
export function ExpandedOne(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<CallingPendingParticipants
|
<CallingPendingParticipants
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { noop } from 'lodash';
|
import { noop } from 'lodash';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { animated, useSpring } from '@react-spring/web';
|
||||||
import { Avatar, AvatarSize } from './Avatar';
|
import { Avatar, AvatarSize } from './Avatar';
|
||||||
import { ContactName } from './conversation/ContactName';
|
import { ContactName } from './conversation/ContactName';
|
||||||
import { InContactsIcon } from './InContactsIcon';
|
import { InContactsIcon } from './InContactsIcon';
|
||||||
|
@ -20,6 +22,9 @@ import type { ServiceIdString } from '../types/ServiceId';
|
||||||
import { handleOutsideClick } from '../util/handleOutsideClick';
|
import { handleOutsideClick } from '../util/handleOutsideClick';
|
||||||
import { Theme } from '../util/theme';
|
import { Theme } from '../util/theme';
|
||||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
|
import { usePrevious } from '../hooks/usePrevious';
|
||||||
|
import { useReducedMotion } from '../hooks/useReducedMotion';
|
||||||
|
import { drop } from '../util/drop';
|
||||||
|
|
||||||
enum ConfirmDialogState {
|
enum ConfirmDialogState {
|
||||||
None = 'None',
|
None = 'None',
|
||||||
|
@ -49,11 +54,33 @@ export function CallingPendingParticipants({
|
||||||
denyUser,
|
denyUser,
|
||||||
toggleCallLinkPendingParticipantModal,
|
toggleCallLinkPendingParticipantModal,
|
||||||
}: PropsType): JSX.Element | null {
|
}: PropsType): JSX.Element | null {
|
||||||
|
const reducedMotion = useReducedMotion();
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
const [opacitySpringProps, opacitySpringApi] = useSpring(
|
||||||
|
{
|
||||||
|
from: { opacity: 0 },
|
||||||
|
to: { opacity: 1 },
|
||||||
|
config: { clamp: true, friction: 22, tension: 360 },
|
||||||
|
immediate: reducedMotion,
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// We show the first pending participant. Save this participant, so if all requests
|
||||||
|
// are resolved then we can keep showing the participant while fading out.
|
||||||
|
const lastParticipantRef = React.useRef<ConversationType | undefined>();
|
||||||
|
lastParticipantRef.current = participants[0] ?? lastParticipantRef.current;
|
||||||
|
const participantCount = participants.length;
|
||||||
|
const prevParticipantCount = usePrevious(participantCount, participantCount);
|
||||||
|
|
||||||
|
const [isVisible, setIsVisible] = useState(participantCount > 0);
|
||||||
const [isExpanded, setIsExpanded] = useState(defaultIsExpanded ?? false);
|
const [isExpanded, setIsExpanded] = useState(defaultIsExpanded ?? false);
|
||||||
const [confirmDialogState, setConfirmDialogState] =
|
const [confirmDialogState, setConfirmDialogState] =
|
||||||
React.useState<ConfirmDialogState>(ConfirmDialogState.None);
|
useState<ConfirmDialogState>(ConfirmDialogState.None);
|
||||||
const [serviceIdsStagedForAction, setServiceIdsStagedForAction] =
|
const [serviceIdsStagedForAction, setServiceIdsStagedForAction] = useState<
|
||||||
React.useState<Array<ServiceIdString>>([]);
|
Array<ServiceIdString>
|
||||||
|
>([]);
|
||||||
|
|
||||||
const expandedListRef = useRef<HTMLDivElement>(null);
|
const expandedListRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
@ -120,7 +147,7 @@ export function CallingPendingParticipants({
|
||||||
}, [serviceIdsStagedForAction, batchUserAction, hideConfirmDialog]);
|
}, [serviceIdsStagedForAction, batchUserAction, hideConfirmDialog]);
|
||||||
|
|
||||||
const renderApprovalButtons = useCallback(
|
const renderApprovalButtons = useCallback(
|
||||||
(participant: ConversationType) => {
|
(participant: ConversationType, isEnabled: boolean = true) => {
|
||||||
if (participant.serviceId == null) {
|
if (participant.serviceId == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -130,7 +157,7 @@ export function CallingPendingParticipants({
|
||||||
<Button
|
<Button
|
||||||
aria-label={i18n('icu:CallingPendingParticipants__DenyUser')}
|
aria-label={i18n('icu:CallingPendingParticipants__DenyUser')}
|
||||||
className="CallingPendingParticipants__PendingActionButton CallingButton__icon"
|
className="CallingPendingParticipants__PendingActionButton CallingButton__icon"
|
||||||
onClick={() => handleDeny(participant)}
|
onClick={isEnabled ? () => handleDeny(participant) : noop}
|
||||||
variant={ButtonVariant.Destructive}
|
variant={ButtonVariant.Destructive}
|
||||||
>
|
>
|
||||||
<span className="CallingPendingParticipants__PendingActionButtonIcon CallingPendingParticipants__PendingActionButtonIcon--Deny" />
|
<span className="CallingPendingParticipants__PendingActionButtonIcon CallingPendingParticipants__PendingActionButtonIcon--Deny" />
|
||||||
|
@ -138,7 +165,7 @@ export function CallingPendingParticipants({
|
||||||
<Button
|
<Button
|
||||||
aria-label={i18n('icu:CallingPendingParticipants__ApproveUser')}
|
aria-label={i18n('icu:CallingPendingParticipants__ApproveUser')}
|
||||||
className="CallingPendingParticipants__PendingActionButton CallingButton__icon"
|
className="CallingPendingParticipants__PendingActionButton CallingButton__icon"
|
||||||
onClick={() => handleApprove(participant)}
|
onClick={isEnabled ? () => handleApprove(participant) : noop}
|
||||||
variant={ButtonVariant.Calling}
|
variant={ButtonVariant.Calling}
|
||||||
>
|
>
|
||||||
<span className="CallingPendingParticipants__PendingActionButtonIcon CallingPendingParticipants__PendingActionButtonIcon--Approve" />
|
<span className="CallingPendingParticipants__PendingActionButtonIcon CallingPendingParticipants__PendingActionButtonIcon--Approve" />
|
||||||
|
@ -165,6 +192,32 @@ export function CallingPendingParticipants({
|
||||||
);
|
);
|
||||||
}, [isExpanded, handleHideAllRequests]);
|
}, [isExpanded, handleHideAllRequests]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (participantCount > prevParticipantCount) {
|
||||||
|
setIsVisible(true);
|
||||||
|
opacitySpringApi.stop();
|
||||||
|
drop(Promise.all(opacitySpringApi.start({ opacity: 1 })));
|
||||||
|
} else if (participantCount === 0) {
|
||||||
|
opacitySpringApi.stop();
|
||||||
|
drop(
|
||||||
|
Promise.all(
|
||||||
|
opacitySpringApi.start({
|
||||||
|
to: { opacity: 0 },
|
||||||
|
onRest: () => {
|
||||||
|
if (!participantCount) {
|
||||||
|
setIsVisible(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [opacitySpringApi, participantCount, prevParticipantCount, setIsVisible]);
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (confirmDialogState === ConfirmDialogState.ApproveAll) {
|
if (confirmDialogState === ConfirmDialogState.ApproveAll) {
|
||||||
return (
|
return (
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
|
@ -228,7 +281,7 @@ export function CallingPendingParticipants({
|
||||||
<div className="module-calling-participants-list__header">
|
<div className="module-calling-participants-list__header">
|
||||||
<div className="module-calling-participants-list__title">
|
<div className="module-calling-participants-list__title">
|
||||||
{i18n('icu:CallingPendingParticipants__RequestsToJoin', {
|
{i18n('icu:CallingPendingParticipants__RequestsToJoin', {
|
||||||
count: participants.length,
|
count: participantCount,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
@ -311,15 +364,33 @@ export function CallingPendingParticipants({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const participant = participants[0];
|
const participant = lastParticipantRef.current;
|
||||||
|
if (!participant) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpandable = participantCount > 1;
|
||||||
return (
|
return (
|
||||||
<div className="CallingPendingParticipants CallingPendingParticipants--Compact module-calling-participants-list">
|
<animated.div
|
||||||
|
className={classNames(
|
||||||
|
'CallingPendingParticipants',
|
||||||
|
'CallingPendingParticipants--Compact',
|
||||||
|
'module-calling-participants-list',
|
||||||
|
isExpandable && 'CallingPendingParticipants--Expandable'
|
||||||
|
)}
|
||||||
|
style={opacitySpringProps}
|
||||||
|
aria-hidden={participantCount === 0}
|
||||||
|
>
|
||||||
<div className="CallingPendingParticipants__CompactParticipant">
|
<div className="CallingPendingParticipants__CompactParticipant">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={ev => {
|
onClick={ev => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
if (participantCount === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
toggleCallLinkPendingParticipantModal(participant.id);
|
toggleCallLinkPendingParticipantModal(participant.id);
|
||||||
}}
|
}}
|
||||||
className="module-calling-participants-list__avatar-and-name CallingPendingParticipants__ParticipantButton"
|
className="module-calling-participants-list__avatar-and-name CallingPendingParticipants__ParticipantButton"
|
||||||
|
@ -353,9 +424,9 @@ export function CallingPendingParticipants({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{renderApprovalButtons(participant)}
|
{renderApprovalButtons(participant, participantCount > 0)}
|
||||||
</div>
|
</div>
|
||||||
{participants.length > 1 && (
|
{isExpandable && (
|
||||||
<div className="CallingPendingParticipants__ShowAllRequestsButtonContainer">
|
<div className="CallingPendingParticipants__ShowAllRequestsButtonContainer">
|
||||||
<button
|
<button
|
||||||
className="CallingPendingParticipants__ShowAllRequestsButton"
|
className="CallingPendingParticipants__ShowAllRequestsButton"
|
||||||
|
@ -363,11 +434,11 @@ export function CallingPendingParticipants({
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{i18n('icu:CallingPendingParticipants__AdditionalRequests', {
|
{i18n('icu:CallingPendingParticipants__AdditionalRequests', {
|
||||||
count: participants.length - 1,
|
count: participantCount - 1,
|
||||||
})}
|
})}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</animated.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3126,5 +3126,13 @@
|
||||||
"line": " message.innerHTML = window.i18n('icu:optimizingApplication');",
|
"line": " message.innerHTML = window.i18n('icu:optimizingApplication');",
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-09-17T21:02:59.414Z"
|
"updated": "2021-09-17T21:02:59.414Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/components/CallingPendingParticipants.tsx",
|
||||||
|
"line": " const lastParticipantRef = React.useRef<ConversationType | undefined>();",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2024-09-20T02:11:27.851Z",
|
||||||
|
"reasonDetail": "For fading out, to keep showing the last known participant"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
Loading…
Add table
Reference in a new issue