Call link batch approve join requests
Co-authored-by: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com>
This commit is contained in:
parent
c046d36851
commit
ea37980fc3
16 changed files with 759 additions and 78 deletions
|
@ -28,6 +28,7 @@ import enMessages from '../../_locales/en/messages.json';
|
|||
import { StorySendMode } from '../types/Stories';
|
||||
import {
|
||||
FAKE_CALL_LINK,
|
||||
FAKE_CALL_LINK_WITH_ADMIN_KEY,
|
||||
getDefaultCallLinkConversation,
|
||||
} from '../test-both/helpers/fakeCallLink';
|
||||
import { allRemoteParticipants } from './CallScreen.stories';
|
||||
|
@ -72,6 +73,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
|||
availableCameras: [],
|
||||
acceptCall: action('accept-call'),
|
||||
approveUser: action('approve-user'),
|
||||
batchUserAction: action('batch-user-action'),
|
||||
bounceAppIconStart: action('bounce-app-icon-start'),
|
||||
bounceAppIconStop: action('bounce-app-icon-stop'),
|
||||
cancelCall: action('cancel-call'),
|
||||
|
@ -145,10 +147,11 @@ const getActiveCallForCallLink = (
|
|||
settingsDialogOpen: false,
|
||||
showParticipantsList: overrideProps.showParticipantsList ?? true,
|
||||
callMode: CallMode.Adhoc,
|
||||
connectionState: GroupCallConnectionState.NotConnected,
|
||||
connectionState:
|
||||
overrideProps.connectionState ?? GroupCallConnectionState.NotConnected,
|
||||
conversationsByDemuxId: new Map<number, ConversationType>(),
|
||||
deviceCount: 0,
|
||||
joinState: GroupCallJoinState.NotJoined,
|
||||
joinState: overrideProps.joinState ?? GroupCallJoinState.NotJoined,
|
||||
localDemuxId: 1,
|
||||
maxDevices: 5,
|
||||
groupMembers: [],
|
||||
|
@ -156,7 +159,7 @@ const getActiveCallForCallLink = (
|
|||
peekedParticipants:
|
||||
overrideProps.peekedParticipants ?? allRemoteParticipants.slice(0, 3),
|
||||
remoteParticipants: overrideProps.remoteParticipants ?? [],
|
||||
pendingParticipants: [],
|
||||
pendingParticipants: overrideProps.pendingParticipants ?? [],
|
||||
raisedHands: new Set<number>(),
|
||||
remoteAudioLevels: new Map<number, number>(),
|
||||
};
|
||||
|
@ -365,3 +368,110 @@ export function CallLinkLobbyParticipants3Unknown(): JSX.Element {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CallLinkWithJoinRequestsOne(): JSX.Element {
|
||||
return (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: getActiveCallForCallLink({
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
peekedParticipants: [allRemoteParticipants[0]],
|
||||
pendingParticipants: [allRemoteParticipants[1]],
|
||||
showParticipantsList: false,
|
||||
}),
|
||||
callLink: FAKE_CALL_LINK_WITH_ADMIN_KEY,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CallLinkWithJoinRequestsTwo(): JSX.Element {
|
||||
return (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: getActiveCallForCallLink({
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
peekedParticipants: [allRemoteParticipants[0]],
|
||||
pendingParticipants: allRemoteParticipants.slice(1, 3),
|
||||
showParticipantsList: false,
|
||||
}),
|
||||
callLink: FAKE_CALL_LINK_WITH_ADMIN_KEY,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CallLinkWithJoinRequestsMany(): JSX.Element {
|
||||
return (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: getActiveCallForCallLink({
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
peekedParticipants: [allRemoteParticipants[0]],
|
||||
pendingParticipants: allRemoteParticipants.slice(1, 11),
|
||||
showParticipantsList: false,
|
||||
}),
|
||||
callLink: FAKE_CALL_LINK_WITH_ADMIN_KEY,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CallLinkWithJoinRequestsSystemContact(): JSX.Element {
|
||||
return (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: getActiveCallForCallLink({
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
peekedParticipants: [allRemoteParticipants[0]],
|
||||
pendingParticipants: [
|
||||
{ ...allRemoteParticipants[1], name: 'My System Contact Friend' },
|
||||
],
|
||||
showParticipantsList: false,
|
||||
}),
|
||||
callLink: FAKE_CALL_LINK_WITH_ADMIN_KEY,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CallLinkWithJoinRequestsSystemContactMany(): JSX.Element {
|
||||
return (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: getActiveCallForCallLink({
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
peekedParticipants: [allRemoteParticipants[0]],
|
||||
pendingParticipants: [
|
||||
{ ...allRemoteParticipants[1], name: 'My System Contact Friend' },
|
||||
allRemoteParticipants[2],
|
||||
allRemoteParticipants[3],
|
||||
],
|
||||
showParticipantsList: false,
|
||||
}),
|
||||
callLink: FAKE_CALL_LINK_WITH_ADMIN_KEY,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CallLinkWithJoinRequestsParticipantsOpen(): JSX.Element {
|
||||
return (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: getActiveCallForCallLink({
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
peekedParticipants: [allRemoteParticipants[0]],
|
||||
pendingParticipants: allRemoteParticipants.slice(1, 4),
|
||||
}),
|
||||
callLink: FAKE_CALL_LINK_WITH_ADMIN_KEY,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type {
|
||||
AcceptCallType,
|
||||
BatchUserActionPayloadType,
|
||||
CancelCallType,
|
||||
DeclineCallType,
|
||||
GroupCallParticipantInfoType,
|
||||
|
@ -101,6 +102,7 @@ export type PropsType = {
|
|||
toggleParticipants: () => void;
|
||||
acceptCall: (_: AcceptCallType) => void;
|
||||
approveUser: (payload: PendingUserActionPayloadType) => void;
|
||||
batchUserAction: (payload: BatchUserActionPayloadType) => void;
|
||||
bounceAppIconStart: () => unknown;
|
||||
bounceAppIconStop: () => unknown;
|
||||
declineCall: (_: DeclineCallType) => void;
|
||||
|
@ -164,6 +166,7 @@ function ActiveCallManager({
|
|||
activeCall,
|
||||
approveUser,
|
||||
availableCameras,
|
||||
batchUserAction,
|
||||
blockClient,
|
||||
callLink,
|
||||
cancelCall,
|
||||
|
@ -445,6 +448,7 @@ function ActiveCallManager({
|
|||
<CallScreen
|
||||
activeCall={activeCall}
|
||||
approveUser={approveUser}
|
||||
batchUserAction={batchUserAction}
|
||||
changeCallView={changeCallView}
|
||||
denyUser={denyUser}
|
||||
getPresentingSources={getPresentingSources}
|
||||
|
@ -519,6 +523,7 @@ export function CallManager({
|
|||
activeCall,
|
||||
approveUser,
|
||||
availableCameras,
|
||||
batchUserAction,
|
||||
blockClient,
|
||||
bounceAppIconStart,
|
||||
bounceAppIconStop,
|
||||
|
@ -618,6 +623,7 @@ export function CallManager({
|
|||
activeCall={activeCall}
|
||||
availableCameras={availableCameras}
|
||||
approveUser={approveUser}
|
||||
batchUserAction={batchUserAction}
|
||||
blockClient={blockClient}
|
||||
callLink={callLink}
|
||||
cancelCall={cancelCall}
|
||||
|
|
|
@ -185,6 +185,7 @@ const createProps = (
|
|||
): PropsType => ({
|
||||
activeCall: createActiveCallProp(overrideProps),
|
||||
approveUser: action('approve-user'),
|
||||
batchUserAction: action('batch-user-action'),
|
||||
changeCallView: action('change-call-view'),
|
||||
denyUser: action('deny-user'),
|
||||
getGroupCallVideoFrameSource: fakeGetGroupCallVideoFrameSource,
|
||||
|
|
|
@ -8,6 +8,7 @@ import classNames from 'classnames';
|
|||
import type { VideoFrameSource } from '@signalapp/ringrtc';
|
||||
import type {
|
||||
ActiveCallStateType,
|
||||
BatchUserActionPayloadType,
|
||||
PendingUserActionPayloadType,
|
||||
SendGroupCallRaiseHandType,
|
||||
SendGroupCallReactionType,
|
||||
|
@ -95,6 +96,7 @@ import type { CallingImageDataCache } from './CallManager';
|
|||
export type PropsType = {
|
||||
activeCall: ActiveCallType;
|
||||
approveUser: (payload: PendingUserActionPayloadType) => void;
|
||||
batchUserAction: (payload: BatchUserActionPayloadType) => void;
|
||||
denyUser: (payload: PendingUserActionPayloadType) => void;
|
||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||
getPresentingSources: () => void;
|
||||
|
@ -186,6 +188,7 @@ function CallDuration({
|
|||
export function CallScreen({
|
||||
activeCall,
|
||||
approveUser,
|
||||
batchUserAction,
|
||||
changeCallView,
|
||||
denyUser,
|
||||
getGroupCallVideoFrameSource,
|
||||
|
@ -856,9 +859,9 @@ export function CallScreen({
|
|||
{pendingParticipants.length ? (
|
||||
<CallingPendingParticipants
|
||||
i18n={i18n}
|
||||
ourServiceId={me.serviceId}
|
||||
participants={pendingParticipants}
|
||||
approveUser={approveUser}
|
||||
batchUserAction={batchUserAction}
|
||||
denyUser={denyUser}
|
||||
/>
|
||||
) : null}
|
||||
|
|
91
ts/components/CallingPendingParticipants.stories.tsx
Normal file
91
ts/components/CallingPendingParticipants.stories.tsx
Normal file
|
@ -0,0 +1,91 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './CallingPendingParticipants';
|
||||
import { CallingPendingParticipants } from './CallingPendingParticipants';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { allRemoteParticipants } from './CallScreen.stories';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
||||
i18n,
|
||||
participants: [allRemoteParticipants[0], allRemoteParticipants[1]],
|
||||
approveUser: action('approve-user'),
|
||||
batchUserAction: action('batch-user-action'),
|
||||
denyUser: action('deny-user'),
|
||||
...storyProps,
|
||||
});
|
||||
|
||||
export default {
|
||||
title: 'Components/CallingPendingParticipants',
|
||||
argTypes: {},
|
||||
args: {},
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
export function One(): JSX.Element {
|
||||
return (
|
||||
<CallingPendingParticipants
|
||||
{...createProps({
|
||||
participants: [allRemoteParticipants[0]],
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Two(): JSX.Element {
|
||||
return (
|
||||
<CallingPendingParticipants
|
||||
{...createProps({
|
||||
participants: allRemoteParticipants.slice(0, 2),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Many(): JSX.Element {
|
||||
return (
|
||||
<CallingPendingParticipants
|
||||
{...createProps({
|
||||
participants: allRemoteParticipants.slice(0, 10),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExpandedOne(): JSX.Element {
|
||||
return (
|
||||
<CallingPendingParticipants
|
||||
{...createProps({
|
||||
defaultIsExpanded: true,
|
||||
participants: [allRemoteParticipants[0]],
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExpandedTwo(): JSX.Element {
|
||||
return (
|
||||
<CallingPendingParticipants
|
||||
{...createProps({
|
||||
defaultIsExpanded: true,
|
||||
participants: allRemoteParticipants.slice(0, 2),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExpandedMany(): JSX.Element {
|
||||
return (
|
||||
<CallingPendingParticipants
|
||||
{...createProps({
|
||||
defaultIsExpanded: true,
|
||||
participants: allRemoteParticipants.slice(0, 10),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -3,99 +3,350 @@
|
|||
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { noop } from 'lodash';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import { ContactName } from './conversation/ContactName';
|
||||
import { InContactsIcon } from './InContactsIcon';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { isInSystemContacts } from '../util/isInSystemContacts';
|
||||
import type { PendingUserActionPayloadType } from '../state/ducks/calling';
|
||||
import type { ServiceIdString } from '../types/ServiceId';
|
||||
import type {
|
||||
BatchUserActionPayloadType,
|
||||
PendingUserActionPayloadType,
|
||||
} from '../state/ducks/calling';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import type { ServiceIdString } from '../types/ServiceId';
|
||||
import { handleOutsideClick } from '../util/handleOutsideClick';
|
||||
import { Theme } from '../util/theme';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
|
||||
enum ConfirmDialogState {
|
||||
None = 'None',
|
||||
ApproveAll = 'ApproveAll',
|
||||
DenyAll = 'DenyAll',
|
||||
}
|
||||
|
||||
export type PropsType = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly ourServiceId: ServiceIdString | undefined;
|
||||
readonly participants: Array<ConversationType>;
|
||||
// For storybook
|
||||
readonly defaultIsExpanded?: boolean;
|
||||
readonly approveUser: (payload: PendingUserActionPayloadType) => void;
|
||||
readonly batchUserAction: (payload: BatchUserActionPayloadType) => void;
|
||||
readonly denyUser: (payload: PendingUserActionPayloadType) => void;
|
||||
};
|
||||
|
||||
export function CallingPendingParticipants({
|
||||
defaultIsExpanded,
|
||||
i18n,
|
||||
ourServiceId,
|
||||
participants,
|
||||
approveUser,
|
||||
batchUserAction,
|
||||
denyUser,
|
||||
}: PropsType): JSX.Element | null {
|
||||
return (
|
||||
<div className="CallingPendingParticipants module-calling-participants-list">
|
||||
<div className="module-calling-participants-list__header">
|
||||
<div className="module-calling-participants-list__title">
|
||||
{i18n('icu:CallingPendingParticipants__RequestsToJoin', {
|
||||
count: participants.length,
|
||||
})}
|
||||
const [isExpanded, setIsExpanded] = useState(defaultIsExpanded ?? false);
|
||||
const [confirmDialogState, setConfirmDialogState] =
|
||||
React.useState<ConfirmDialogState>(ConfirmDialogState.None);
|
||||
const [serviceIdsStagedForAction, setServiceIdsStagedForAction] =
|
||||
React.useState<Array<ServiceIdString>>([]);
|
||||
|
||||
const expandedListRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleHideAllRequests = useCallback(() => {
|
||||
setIsExpanded(false);
|
||||
}, [setIsExpanded]);
|
||||
|
||||
// When opening the "Approve all" confirm dialog, save the current list of participants
|
||||
// to ensure we only approve users who the admin has checked. If additional people
|
||||
// request to join while the dialog is open, we don't auto approve those.
|
||||
const stageServiceIdsForAction = useCallback(() => {
|
||||
const serviceIds: Array<ServiceIdString> = [];
|
||||
participants.forEach(participant => {
|
||||
if (participant.serviceId) {
|
||||
serviceIds.push(participant.serviceId);
|
||||
}
|
||||
});
|
||||
setServiceIdsStagedForAction(serviceIds);
|
||||
}, [participants, setServiceIdsStagedForAction]);
|
||||
|
||||
const hideConfirmDialog = useCallback(() => {
|
||||
setConfirmDialogState(ConfirmDialogState.None);
|
||||
setServiceIdsStagedForAction([]);
|
||||
}, [setConfirmDialogState]);
|
||||
|
||||
const handleApprove = useCallback(
|
||||
(participant: ConversationType) => {
|
||||
const { serviceId } = participant;
|
||||
if (!serviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
approveUser({ serviceId });
|
||||
},
|
||||
[approveUser]
|
||||
);
|
||||
|
||||
const handleDeny = useCallback(
|
||||
(participant: ConversationType) => {
|
||||
const { serviceId } = participant;
|
||||
if (!serviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
denyUser({ serviceId });
|
||||
},
|
||||
[denyUser]
|
||||
);
|
||||
|
||||
const handleApproveAll = useCallback(() => {
|
||||
batchUserAction({
|
||||
action: 'approve',
|
||||
serviceIds: serviceIdsStagedForAction,
|
||||
});
|
||||
hideConfirmDialog();
|
||||
}, [serviceIdsStagedForAction, batchUserAction, hideConfirmDialog]);
|
||||
|
||||
const handleDenyAll = useCallback(() => {
|
||||
batchUserAction({
|
||||
action: 'deny',
|
||||
serviceIds: serviceIdsStagedForAction,
|
||||
});
|
||||
hideConfirmDialog();
|
||||
}, [serviceIdsStagedForAction, batchUserAction, hideConfirmDialog]);
|
||||
|
||||
const renderApprovalButtons = useCallback(
|
||||
(participant: ConversationType) => {
|
||||
if (participant.serviceId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
aria-label={i18n('icu:CallingPendingParticipants__DenyUser')}
|
||||
className="CallingPendingParticipants__PendingActionButton CallingButton__icon"
|
||||
onClick={() => handleDeny(participant)}
|
||||
variant={ButtonVariant.Destructive}
|
||||
>
|
||||
<span className="CallingPendingParticipants__PendingActionButtonIcon CallingPendingParticipants__PendingActionButtonIcon--Deny" />
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={i18n('icu:CallingPendingParticipants__ApproveUser')}
|
||||
className="CallingPendingParticipants__PendingActionButton CallingButton__icon"
|
||||
onClick={() => handleApprove(participant)}
|
||||
variant={ButtonVariant.Calling}
|
||||
>
|
||||
<span className="CallingPendingParticipants__PendingActionButtonIcon CallingPendingParticipants__PendingActionButtonIcon--Approve" />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
[i18n, handleApprove, handleDeny]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isExpanded) {
|
||||
return noop;
|
||||
}
|
||||
return handleOutsideClick(
|
||||
() => {
|
||||
handleHideAllRequests();
|
||||
return true;
|
||||
},
|
||||
{
|
||||
containerElements: [expandedListRef],
|
||||
name: 'CallingPendingParticipantsList.expandedList',
|
||||
}
|
||||
);
|
||||
}, [isExpanded, handleHideAllRequests]);
|
||||
|
||||
if (confirmDialogState === ConfirmDialogState.ApproveAll) {
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
dialogName="CallingPendingParticipants.confirmDialog"
|
||||
actions={[
|
||||
{
|
||||
action: handleApproveAll,
|
||||
style: 'affirmative',
|
||||
text: i18n('icu:CallingPendingParticipants__ApproveAll'),
|
||||
},
|
||||
]}
|
||||
cancelText={i18n('icu:cancel')}
|
||||
i18n={i18n}
|
||||
theme={Theme.Dark}
|
||||
title={i18n(
|
||||
'icu:CallingPendingParticipants__ConfirmDialogTitle--ApproveAll',
|
||||
{ count: serviceIdsStagedForAction.length }
|
||||
)}
|
||||
onClose={hideConfirmDialog}
|
||||
>
|
||||
{i18n('icu:CallingPendingParticipants__ConfirmDialogBody--ApproveAll', {
|
||||
count: serviceIdsStagedForAction.length,
|
||||
})}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
if (confirmDialogState === ConfirmDialogState.DenyAll) {
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
dialogName="CallingPendingParticipants.confirmDialog"
|
||||
actions={[
|
||||
{
|
||||
action: handleDenyAll,
|
||||
style: 'affirmative',
|
||||
text: i18n('icu:CallingPendingParticipants__DenyAll'),
|
||||
},
|
||||
]}
|
||||
cancelText={i18n('icu:cancel')}
|
||||
i18n={i18n}
|
||||
theme={Theme.Dark}
|
||||
title={i18n(
|
||||
'icu:CallingPendingParticipants__ConfirmDialogTitle--DenyAll',
|
||||
{ count: serviceIdsStagedForAction.length }
|
||||
)}
|
||||
onClose={hideConfirmDialog}
|
||||
>
|
||||
{i18n('icu:CallingPendingParticipants__ConfirmDialogBody--DenyAll', {
|
||||
count: serviceIdsStagedForAction.length,
|
||||
})}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
if (isExpanded) {
|
||||
return (
|
||||
<div
|
||||
className="CallingPendingParticipants CallingPendingParticipants--Expanded module-calling-participants-list"
|
||||
ref={expandedListRef}
|
||||
>
|
||||
<div className="module-calling-participants-list__header">
|
||||
<div className="module-calling-participants-list__title">
|
||||
{i18n('icu:CallingPendingParticipants__RequestsToJoin', {
|
||||
count: participants.length,
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="module-calling-participants-list__close"
|
||||
onClick={handleHideAllRequests}
|
||||
tabIndex={0}
|
||||
aria-label={i18n('icu:close')}
|
||||
/>
|
||||
</div>
|
||||
<ul className="module-calling-participants-list__list">
|
||||
{participants.map((participant: ConversationType, index: number) => (
|
||||
<li
|
||||
className="module-calling-participants-list__contact"
|
||||
key={index}
|
||||
>
|
||||
<div className="module-calling-participants-list__avatar-and-name">
|
||||
<Avatar
|
||||
acceptedMessageRequest={participant.acceptedMessageRequest}
|
||||
avatarPath={participant.avatarPath}
|
||||
badge={undefined}
|
||||
color={participant.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={participant.isMe}
|
||||
profileName={participant.profileName}
|
||||
title={participant.title}
|
||||
sharedGroupNames={participant.sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_SIX}
|
||||
/>
|
||||
<ContactName
|
||||
module="module-calling-participants-list__name"
|
||||
title={participant.title}
|
||||
/>
|
||||
{isInSystemContacts(participant) ? (
|
||||
<span>
|
||||
{' '}
|
||||
<InContactsIcon
|
||||
className="module-calling-participants-list__contact-icon"
|
||||
i18n={i18n}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{renderApprovalButtons(participant)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="CallingPendingParticipants__ActionPanel">
|
||||
<Button
|
||||
className="CallingPendingParticipants__ActionPanelButton CallingPendingParticipants__ActionPanelButton--DenyAll"
|
||||
variant={ButtonVariant.Destructive}
|
||||
onClick={() => {
|
||||
stageServiceIdsForAction();
|
||||
setConfirmDialogState(ConfirmDialogState.DenyAll);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:CallingPendingParticipants__DenyAll')}
|
||||
</Button>
|
||||
<Button
|
||||
className="CallingPendingParticipants__ActionPanelButton CallingPendingParticipants__ActionPanelButton--ApproveAll"
|
||||
variant={ButtonVariant.Calling}
|
||||
onClick={() => {
|
||||
stageServiceIdsForAction();
|
||||
setConfirmDialogState(ConfirmDialogState.ApproveAll);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:CallingPendingParticipants__ApproveAll')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="module-calling-participants-list__list">
|
||||
{participants.map((participant: ConversationType, index: number) => (
|
||||
<li className="module-calling-participants-list__contact" key={index}>
|
||||
<div className="module-calling-participants-list__avatar-and-name">
|
||||
<Avatar
|
||||
acceptedMessageRequest={participant.acceptedMessageRequest}
|
||||
avatarPath={participant.avatarPath}
|
||||
badge={undefined}
|
||||
color={participant.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={participant.isMe}
|
||||
profileName={participant.profileName}
|
||||
title={participant.title}
|
||||
sharedGroupNames={participant.sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
/>
|
||||
{ourServiceId && participant.serviceId === ourServiceId ? (
|
||||
<span className="module-calling-participants-list__name">
|
||||
{i18n('icu:you')}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<ContactName
|
||||
module="module-calling-participants-list__name"
|
||||
title={participant.title}
|
||||
/>
|
||||
{isInSystemContacts(participant) ? (
|
||||
<span>
|
||||
{' '}
|
||||
<InContactsIcon
|
||||
className="module-calling-participants-list__contact-icon"
|
||||
i18n={i18n}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
);
|
||||
}
|
||||
|
||||
const participant = participants[0];
|
||||
return (
|
||||
<div className="CallingPendingParticipants CallingPendingParticipants--Compact module-calling-participants-list">
|
||||
<div className="CallingPendingParticipants__CompactParticipant">
|
||||
<div className="module-calling-participants-list__avatar-and-name">
|
||||
<Avatar
|
||||
acceptedMessageRequest={participant.acceptedMessageRequest}
|
||||
avatarPath={participant.avatarPath}
|
||||
badge={undefined}
|
||||
color={participant.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={participant.isMe}
|
||||
profileName={participant.profileName}
|
||||
title={participant.title}
|
||||
sharedGroupNames={participant.sharedGroupNames}
|
||||
size={AvatarSize.FORTY_EIGHT}
|
||||
/>
|
||||
<div className="CallingPendingParticipants__CompactParticipantNameColumn">
|
||||
<div className="CallingPendingParticipants__ParticipantName">
|
||||
<ContactName title={participant.title} />
|
||||
{isInSystemContacts(participant) ? (
|
||||
<InContactsIcon
|
||||
className="module-calling-participants-list__contact-icon"
|
||||
i18n={i18n}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<Button
|
||||
aria-label={i18n('icu:CallingPendingParticipants__DenyUser')}
|
||||
className="CallingPendingParticipants__PendingActionButton CallingButton__icon"
|
||||
onClick={() => denyUser({ serviceId: participant.serviceId })}
|
||||
variant={ButtonVariant.Destructive}
|
||||
>
|
||||
<span className="CallingPendingParticipants__PendingActionButtonIcon CallingPendingParticipants__PendingActionButtonIcon--Deny" />
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={i18n('icu:CallingPendingParticipants__ApproveUser')}
|
||||
className="CallingPendingParticipants__PendingActionButton CallingButton__icon"
|
||||
onClick={() => approveUser({ serviceId: participant.serviceId })}
|
||||
variant={ButtonVariant.Calling}
|
||||
>
|
||||
<span className="CallingPendingParticipants__PendingActionButtonIcon CallingPendingParticipants__PendingActionButtonIcon--Approve" />
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="CallingPendingParticipants__WouldLikeToJoin">
|
||||
{i18n('icu:CallingPendingParticipants__WouldLikeToJoin')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{renderApprovalButtons(participant)}
|
||||
</div>
|
||||
{participants.length > 1 && (
|
||||
<div className="CallingPendingParticipants__ShowAllRequestsButtonContainer">
|
||||
<button
|
||||
className="CallingPendingParticipants__ShowAllRequestsButton"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
type="button"
|
||||
>
|
||||
{i18n('icu:CallingPendingParticipants__AdditionalRequests', {
|
||||
count: participants.length - 1,
|
||||
})}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ function getToast(toastType: ToastType): AnyToast {
|
|||
switch (toastType) {
|
||||
case ToastType.AddingUserToGroup:
|
||||
return { toastType, parameters: { contact: 'Sam Mirete' } };
|
||||
case ToastType.AddedUsersToCall:
|
||||
return { toastType, parameters: { count: 6 } };
|
||||
case ToastType.AlreadyGroupMember:
|
||||
return { toastType: ToastType.AlreadyGroupMember };
|
||||
case ToastType.AlreadyRequestedToJoin:
|
||||
|
|
|
@ -59,6 +59,16 @@ export function renderToast({
|
|||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.AddedUsersToCall) {
|
||||
return (
|
||||
<Toast onClose={hideToast} timeout={SHORT_TIMEOUT}>
|
||||
{i18n('icu:CallingPendingParticipants__Toast--added-users-to-call', {
|
||||
count: toast.parameters.count,
|
||||
})}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.AlreadyGroupMember) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
|
|
|
@ -782,6 +782,11 @@ export type PendingUserActionPayloadType = ReadonlyDeep<{
|
|||
serviceId: ServiceIdString | undefined;
|
||||
}>;
|
||||
|
||||
export type BatchUserActionPayloadType = ReadonlyDeep<{
|
||||
action: 'approve' | 'deny';
|
||||
serviceIds: Array<ServiceIdString>;
|
||||
}>;
|
||||
|
||||
// eslint-disable-next-line local-rules/type-alias-readonlydeep
|
||||
type RefreshIODevicesActionType = {
|
||||
type: 'calling/REFRESH_IO_DEVICES';
|
||||
|
@ -1000,6 +1005,54 @@ function denyUser(
|
|||
dispatch({ type: DENY_USER });
|
||||
};
|
||||
}
|
||||
|
||||
function batchUserAction(
|
||||
payload: BatchUserActionPayloadType
|
||||
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
|
||||
return (dispatch, getState) => {
|
||||
const activeCall = getActiveCall(getState().calling);
|
||||
if (!activeCall || !isGroupOrAdhocCallMode(activeCall.callMode)) {
|
||||
log.warn(
|
||||
'batchUserAction: Trying to do pending user without active group or adhoc call'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { action, serviceIds } = payload;
|
||||
let actionFn;
|
||||
if (action === 'approve') {
|
||||
actionFn = calling.approveUser;
|
||||
} else if (action === 'deny') {
|
||||
actionFn = calling.denyUser;
|
||||
} else {
|
||||
throw missingCaseError(action);
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const serviceId of serviceIds) {
|
||||
if (!isAciString(serviceId)) {
|
||||
log.warn(
|
||||
'batchUserAction: Trying to do user action without valid aci serviceid'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
actionFn.call(calling, activeCall.conversationId, serviceId);
|
||||
count += 1;
|
||||
}
|
||||
|
||||
if (count > 0 && action === 'approve') {
|
||||
dispatch({
|
||||
type: SHOW_TOAST,
|
||||
payload: {
|
||||
toastType: ToastType.AddedUsersToCall,
|
||||
parameters: { count },
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function removeClient(
|
||||
payload: RemoveClientType
|
||||
): ThunkAction<void, RootStateType, unknown, RemoveClientActionType> {
|
||||
|
@ -2296,6 +2349,7 @@ function switchFromPresentationView(): SwitchFromPresentationViewActionType {
|
|||
export const actions = {
|
||||
acceptCall,
|
||||
approveUser,
|
||||
batchUserAction,
|
||||
blockClient,
|
||||
callStateChange,
|
||||
cancelCall,
|
||||
|
|
|
@ -426,6 +426,7 @@ export const SmartCallManager = memo(function SmartCallManager() {
|
|||
|
||||
const {
|
||||
approveUser,
|
||||
batchUserAction,
|
||||
denyUser,
|
||||
changeCallView,
|
||||
closeNeedPermissionScreen,
|
||||
|
@ -465,6 +466,7 @@ export const SmartCallManager = memo(function SmartCallManager() {
|
|||
activeCall={activeCall}
|
||||
approveUser={approveUser}
|
||||
availableCameras={availableCameras}
|
||||
batchUserAction={batchUserAction}
|
||||
blockClient={blockClient}
|
||||
bounceAppIconStart={bounceAppIconStart}
|
||||
bounceAppIconStop={bounceAppIconStop}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
export enum ToastType {
|
||||
AddingUserToGroup = 'AddingUserToGroup',
|
||||
AddedUsersToCall = 'AddedUsersToCall',
|
||||
AlreadyGroupMember = 'AlreadyGroupMember',
|
||||
AlreadyRequestedToJoin = 'AlreadyRequestedToJoin',
|
||||
Blocked = 'Blocked',
|
||||
|
@ -69,6 +70,10 @@ export enum ToastType {
|
|||
|
||||
export type AnyToast =
|
||||
| { toastType: ToastType.AddingUserToGroup; parameters: { contact: string } }
|
||||
| {
|
||||
toastType: ToastType.AddedUsersToCall;
|
||||
parameters: { count: number };
|
||||
}
|
||||
| { toastType: ToastType.AlreadyGroupMember }
|
||||
| { toastType: ToastType.AlreadyRequestedToJoin }
|
||||
| { toastType: ToastType.Blocked }
|
||||
|
|
|
@ -3250,6 +3250,14 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-07-30T16:57:33.618Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingPendingParticipants.tsx",
|
||||
"line": " const expandedListRef = useRef<HTMLDivElement>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-06-28T01:22:22.509Z",
|
||||
"reasonDetail": "For outside click handling"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingPip.tsx",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue