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
|
@ -1733,10 +1733,46 @@
|
|||
"messageformat": "Deny join request",
|
||||
"description": "Tooltip label for check mark button to deny a user's request to join a call."
|
||||
},
|
||||
"icu:CallingPendingParticipants__ApproveAll": {
|
||||
"messageformat": "Approve all",
|
||||
"description": "Button to approve all pending requests to join a call."
|
||||
},
|
||||
"icu:CallingPendingParticipants__DenyAll": {
|
||||
"messageformat": "Deny all",
|
||||
"description": "Button to deny all pending requests to join a call."
|
||||
},
|
||||
"icu:CallingPendingParticipants__ConfirmDialogTitle--ApproveAll": {
|
||||
"messageformat": "Approve {count, plural, one {# request} other {# requests}}?",
|
||||
"description": "Title of confirmation dialog when approving all pending requests to join a call."
|
||||
},
|
||||
"icu:CallingPendingParticipants__ConfirmDialogTitle--DenyAll": {
|
||||
"messageformat": "Deny {count, plural, one {# request} other {# requests}}?",
|
||||
"description": "Title of confirmation dialog when approving all pending requests to join a call."
|
||||
},
|
||||
"icu:CallingPendingParticipants__ConfirmDialogBody--ApproveAll": {
|
||||
"messageformat": "{count, plural, one {# person} other {# people}} will be added to the call.",
|
||||
"description": "Title of confirmation dialog when approving all pending requests to join a call."
|
||||
},
|
||||
"icu:CallingPendingParticipants__ConfirmDialogBody--DenyAll": {
|
||||
"messageformat": "{count, plural, one {# person} other {# people}} will not be added to the call.",
|
||||
"description": "Title of confirmation dialog when denying all pending requests to join a call."
|
||||
},
|
||||
"icu:CallingPendingParticipants__RequestsToJoin": {
|
||||
"messageformat": "{count, plural, one {# request} other {# requests}} to join the call",
|
||||
"description": "Shown in the call pending join request list to describe how many people are requesting to join"
|
||||
},
|
||||
"icu:CallingPendingParticipants__WouldLikeToJoin": {
|
||||
"messageformat": "Would like to join...",
|
||||
"description": "Participant info text in the call pending join requests compact list, visible only to call admins. Indicates that the participant would like to join the call and is requesting action."
|
||||
},
|
||||
"icu:CallingPendingParticipants__AdditionalRequests": {
|
||||
"messageformat": "{count, plural, one {+# request} other {+# requests}}",
|
||||
"description": "Button text in the call pending join requests compact list. When clicked the list expands to the full list"
|
||||
},
|
||||
"icu:CallingPendingParticipants__Toast--added-users-to-call": {
|
||||
"messageformat": "{count, plural, one {# person} other {# people}} added to the call",
|
||||
"description": "Shown in toast after batch approving requests to join a call"
|
||||
},
|
||||
"icu:CallingRaisedHandsList__Title": {
|
||||
"messageformat": "{count, plural, one {# raised hand} other {# raised hands}}",
|
||||
"description": "Shown in the call raised hands list to describe how many people have active raised hands"
|
||||
|
|
3
images/icons/v3/check/check-bold.svg
Normal file
3
images/icons/v3/check/check-bold.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.0391 5.15773C19.5043 5.45544 19.64 6.07388 19.3423 6.53906L11.0223 19.5391C10.8475 19.8121 10.5512 19.9835 10.2274 19.9989C9.90354 20.0142 9.59235 19.8716 9.39254 19.6163L4.71254 13.6363C4.37216 13.2014 4.44881 12.5729 4.88374 12.2325C5.31866 11.8921 5.94717 11.9688 6.28755 12.4037L10.0982 17.2728L17.6578 5.46095C17.9555 4.99577 18.5739 4.86002 19.0391 5.15773Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 536 B |
3
images/icons/v3/x/x-bold.svg
Normal file
3
images/icons/v3/x/x-bold.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.7071 6.70711C19.0976 6.31658 19.0976 5.68342 18.7071 5.29289C18.3166 4.90237 17.6834 4.90237 17.2929 5.29289L12 10.5858L6.70711 5.29289C6.31658 4.90237 5.68342 4.90237 5.29289 5.29289C4.90237 5.68342 4.90237 6.31658 5.29289 6.70711L10.5858 12L5.29289 17.2929C4.90237 17.6834 4.90237 18.3166 5.29289 18.7071C5.68342 19.0976 6.31658 19.0976 6.70711 18.7071L12 13.4142L17.2929 18.7071C17.6834 19.0976 18.3166 19.0976 18.7071 18.7071C19.0976 18.3166 19.0976 17.6834 18.7071 17.2929L13.4142 12L18.7071 6.70711Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 638 B |
|
@ -4,31 +4,127 @@
|
|||
.CallingPendingParticipants {
|
||||
width: 420px;
|
||||
height: auto;
|
||||
padding-block-end: 2px;
|
||||
margin-block-start: auto;
|
||||
margin-block-end: 36px;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants--Compact {
|
||||
width: 364px;
|
||||
padding-inline: 0;
|
||||
padding-block-start: 0;
|
||||
background: $color-gray-78;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants--Expanded {
|
||||
padding-block-end: 2px;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__CompactParticipant {
|
||||
display: flex;
|
||||
padding-block: 12px;
|
||||
padding-inline: 12px;
|
||||
outline: none;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__CompactParticipantNameColumn {
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__ParticipantName {
|
||||
@include font-body-1-bold;
|
||||
color: $color-gray-15;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__WouldLikeToJoin {
|
||||
@include font-body-2;
|
||||
color: $color-gray-20;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__PendingActionButton {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
padding-inline: 0;
|
||||
margin-inline-end: 16px;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__PendingActionButton:first-child {
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__PendingActionButton:last-child {
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__PendingActionButtonIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants--Compact {
|
||||
.CallingPendingParticipants__PendingActionButton {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-inline-end: 20px;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__PendingActionButton:last-child {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__PendingActionButtonIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__PendingActionButtonIcon--Approve {
|
||||
@include color-svg('../images/icons/v3/check/check.svg', $color-white);
|
||||
@include color-svg('../images/icons/v3/check/check-bold.svg', $color-white);
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__PendingActionButtonIcon--Deny {
|
||||
@include color-svg('../images/icons/v3/x/x.svg', $color-white);
|
||||
@include color-svg('../images/icons/v3/x/x-bold.svg', $color-white);
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__ActionPanel {
|
||||
padding-block: 15px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__ActionPanelButton {
|
||||
border-radius: 4px;
|
||||
margin-inline-end: 16px;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__ActionPanelButton:last-child {
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__ShowAllRequestsButton {
|
||||
@include button-reset;
|
||||
@include font-body-2-bold;
|
||||
display: flex;
|
||||
padding-block: 5px;
|
||||
padding-inline: 15px;
|
||||
margin-block: 12px;
|
||||
margin-inline: auto;
|
||||
color: $color-white-alpha-90;
|
||||
background: $color-gray-65;
|
||||
border-radius: 46px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__ShowAllRequestsButton:focus {
|
||||
@include keyboard-mode {
|
||||
outline: 3px solid $color-ultramarine;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.CallingPendingParticipants__ShowAllRequestsButtonContainer {
|
||||
background: $color-black-alpha-24;
|
||||
}
|
||||
|
|
|
@ -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…
Reference in a new issue