Animate call link join requests

This commit is contained in:
ayumi-signal 2024-09-24 11:33:58 -07:00 committed by GitHub
parent 7e9773a144
commit e51cde1770
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 117 additions and 15 deletions

View file

@ -14,10 +14,13 @@
width: 364px;
padding-inline: 0;
padding-block-start: 0;
background: $color-gray-78;
outline: 0;
}
.CallingPendingParticipants--Expandable {
background: $color-gray-78;
}
.CallingPendingParticipants--Expanded {
padding-block-end: 2px;
}

View file

@ -858,7 +858,7 @@ export function CallScreen({
renderRaisedHandsToast={renderRaisedHandsToast}
i18n={i18n}
/>
{pendingParticipants.length ? (
{isCallLinkAdmin ? (
<CallingPendingParticipants
i18n={i18n}
participants={pendingParticipants}

View file

@ -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 {
return (
<CallingPendingParticipants

View file

@ -5,6 +5,8 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { noop } from 'lodash';
import classNames from 'classnames';
import { animated, useSpring } from '@react-spring/web';
import { Avatar, AvatarSize } from './Avatar';
import { ContactName } from './conversation/ContactName';
import { InContactsIcon } from './InContactsIcon';
@ -20,6 +22,9 @@ import type { ServiceIdString } from '../types/ServiceId';
import { handleOutsideClick } from '../util/handleOutsideClick';
import { Theme } from '../util/theme';
import { ConfirmationDialog } from './ConfirmationDialog';
import { usePrevious } from '../hooks/usePrevious';
import { useReducedMotion } from '../hooks/useReducedMotion';
import { drop } from '../util/drop';
enum ConfirmDialogState {
None = 'None',
@ -49,11 +54,33 @@ export function CallingPendingParticipants({
denyUser,
toggleCallLinkPendingParticipantModal,
}: 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 [confirmDialogState, setConfirmDialogState] =
React.useState<ConfirmDialogState>(ConfirmDialogState.None);
const [serviceIdsStagedForAction, setServiceIdsStagedForAction] =
React.useState<Array<ServiceIdString>>([]);
useState<ConfirmDialogState>(ConfirmDialogState.None);
const [serviceIdsStagedForAction, setServiceIdsStagedForAction] = useState<
Array<ServiceIdString>
>([]);
const expandedListRef = useRef<HTMLDivElement>(null);
@ -120,7 +147,7 @@ export function CallingPendingParticipants({
}, [serviceIdsStagedForAction, batchUserAction, hideConfirmDialog]);
const renderApprovalButtons = useCallback(
(participant: ConversationType) => {
(participant: ConversationType, isEnabled: boolean = true) => {
if (participant.serviceId == null) {
return null;
}
@ -130,7 +157,7 @@ export function CallingPendingParticipants({
<Button
aria-label={i18n('icu:CallingPendingParticipants__DenyUser')}
className="CallingPendingParticipants__PendingActionButton CallingButton__icon"
onClick={() => handleDeny(participant)}
onClick={isEnabled ? () => handleDeny(participant) : noop}
variant={ButtonVariant.Destructive}
>
<span className="CallingPendingParticipants__PendingActionButtonIcon CallingPendingParticipants__PendingActionButtonIcon--Deny" />
@ -138,7 +165,7 @@ export function CallingPendingParticipants({
<Button
aria-label={i18n('icu:CallingPendingParticipants__ApproveUser')}
className="CallingPendingParticipants__PendingActionButton CallingButton__icon"
onClick={() => handleApprove(participant)}
onClick={isEnabled ? () => handleApprove(participant) : noop}
variant={ButtonVariant.Calling}
>
<span className="CallingPendingParticipants__PendingActionButtonIcon CallingPendingParticipants__PendingActionButtonIcon--Approve" />
@ -165,6 +192,32 @@ export function CallingPendingParticipants({
);
}, [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) {
return (
<ConfirmationDialog
@ -228,7 +281,7 @@ export function CallingPendingParticipants({
<div className="module-calling-participants-list__header">
<div className="module-calling-participants-list__title">
{i18n('icu:CallingPendingParticipants__RequestsToJoin', {
count: participants.length,
count: participantCount,
})}
</div>
<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 (
<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">
<button
type="button"
onClick={ev => {
ev.preventDefault();
ev.stopPropagation();
if (participantCount === 0) {
return;
}
toggleCallLinkPendingParticipantModal(participant.id);
}}
className="module-calling-participants-list__avatar-and-name CallingPendingParticipants__ParticipantButton"
@ -353,9 +424,9 @@ export function CallingPendingParticipants({
</div>
</div>
</button>
{renderApprovalButtons(participant)}
{renderApprovalButtons(participant, participantCount > 0)}
</div>
{participants.length > 1 && (
{isExpandable && (
<div className="CallingPendingParticipants__ShowAllRequestsButtonContainer">
<button
className="CallingPendingParticipants__ShowAllRequestsButton"
@ -363,11 +434,11 @@ export function CallingPendingParticipants({
type="button"
>
{i18n('icu:CallingPendingParticipants__AdditionalRequests', {
count: participants.length - 1,
count: participantCount - 1,
})}
</button>
</div>
)}
</div>
</animated.div>
);
}

View file

@ -3126,5 +3126,13 @@
"line": " message.innerHTML = window.i18n('icu:optimizingApplication');",
"reasonCategory": "usageTrusted",
"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"
}
]