Call link admin key fix and in-call approve, deny, remove
This commit is contained in:
parent
5df8924197
commit
8ec585d54c
20 changed files with 599 additions and 43 deletions
|
@ -59,12 +59,14 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
|||
...storyProps,
|
||||
availableCameras: [],
|
||||
acceptCall: action('accept-call'),
|
||||
approveUser: action('approve-user'),
|
||||
bounceAppIconStart: action('bounce-app-icon-start'),
|
||||
bounceAppIconStop: action('bounce-app-icon-stop'),
|
||||
cancelCall: action('cancel-call'),
|
||||
changeCallView: action('change-call-view'),
|
||||
closeNeedPermissionScreen: action('close-need-permission-screen'),
|
||||
declineCall: action('decline-call'),
|
||||
denyUser: action('deny-user'),
|
||||
getGroupCallVideoFrameSource: (_: string, demuxId: number) =>
|
||||
fakeGetGroupCallVideoFrameSource(demuxId),
|
||||
getPresentingSources: action('get-presenting-sources'),
|
||||
|
@ -84,6 +86,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
|||
notifyForCall: action('notify-for-call'),
|
||||
openSystemPreferencesAction: action('open-system-preferences-action'),
|
||||
playRingtone: action('play-ringtone'),
|
||||
removeClient: action('remove-client'),
|
||||
renderDeviceSelection: () => <div />,
|
||||
renderEmojiPicker: () => <>EmojiPicker</>,
|
||||
renderReactionPicker: () => <div />,
|
||||
|
@ -156,6 +159,7 @@ export function OngoingGroupCall(): JSX.Element {
|
|||
groupMembers: [],
|
||||
isConversationTooBigToRing: false,
|
||||
peekedParticipants: [],
|
||||
pendingParticipants: [],
|
||||
raisedHands: new Set<number>(),
|
||||
remoteParticipants: [],
|
||||
remoteAudioLevels: new Map<number, number>(),
|
||||
|
|
|
@ -31,6 +31,8 @@ import type {
|
|||
CancelCallType,
|
||||
DeclineCallType,
|
||||
GroupCallParticipantInfoType,
|
||||
PendingUserActionPayloadType,
|
||||
RemoveClientType,
|
||||
SendGroupCallRaiseHandType,
|
||||
SendGroupCallReactionType,
|
||||
SetGroupCallVideoRequestType,
|
||||
|
@ -95,9 +97,11 @@ export type PropsType = {
|
|||
startCall: (payload: StartCallType) => void;
|
||||
toggleParticipants: () => void;
|
||||
acceptCall: (_: AcceptCallType) => void;
|
||||
approveUser: (payload: PendingUserActionPayloadType) => void;
|
||||
bounceAppIconStart: () => unknown;
|
||||
bounceAppIconStop: () => unknown;
|
||||
declineCall: (_: DeclineCallType) => void;
|
||||
denyUser: (payload: PendingUserActionPayloadType) => void;
|
||||
hasInitialLoadCompleted: boolean;
|
||||
i18n: LocalizerType;
|
||||
isGroupCallRaiseHandEnabled: boolean;
|
||||
|
@ -109,6 +113,7 @@ export type PropsType = {
|
|||
) => unknown;
|
||||
openSystemPreferencesAction: () => unknown;
|
||||
playRingtone: () => unknown;
|
||||
removeClient: (payload: RemoveClientType) => void;
|
||||
sendGroupCallRaiseHand: (payload: SendGroupCallRaiseHandType) => void;
|
||||
sendGroupCallReaction: (payload: SendGroupCallReactionType) => void;
|
||||
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
|
||||
|
@ -151,11 +156,13 @@ type ActiveCallManagerPropsType = {
|
|||
|
||||
function ActiveCallManager({
|
||||
activeCall,
|
||||
approveUser,
|
||||
availableCameras,
|
||||
callLink,
|
||||
cancelCall,
|
||||
changeCallView,
|
||||
closeNeedPermissionScreen,
|
||||
denyUser,
|
||||
hangUpActiveCall,
|
||||
i18n,
|
||||
isGroupCallRaiseHandEnabled,
|
||||
|
@ -166,6 +173,7 @@ function ActiveCallManager({
|
|||
renderDeviceSelection,
|
||||
renderEmojiPicker,
|
||||
renderReactionPicker,
|
||||
removeClient,
|
||||
sendGroupCallRaiseHand,
|
||||
sendGroupCallReaction,
|
||||
setGroupCallVideoRequest,
|
||||
|
@ -258,6 +266,7 @@ function ActiveCallManager({
|
|||
let isConvoTooBigToRing = false;
|
||||
let isAdhocAdminApprovalRequired = false;
|
||||
let isAdhocJoinRequestPending = false;
|
||||
let isCallLinkAdmin = false;
|
||||
|
||||
switch (activeCall.callMode) {
|
||||
case CallMode.Direct: {
|
||||
|
@ -292,6 +301,7 @@ function ActiveCallManager({
|
|||
isAdhocJoinRequestPending =
|
||||
isAdhocAdminApprovalRequired &&
|
||||
activeCall.joinState === GroupCallJoinState.Pending;
|
||||
isCallLinkAdmin = Boolean(callLink?.adminKey);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
@ -352,10 +362,12 @@ function ActiveCallManager({
|
|||
<CallingAdhocCallInfo
|
||||
callLink={callLink}
|
||||
i18n={i18n}
|
||||
isCallLinkAdmin={isCallLinkAdmin}
|
||||
ourServiceId={me.serviceId}
|
||||
participants={peekedParticipants}
|
||||
onClose={toggleParticipants}
|
||||
onCopyCallLink={onCopyCallLink}
|
||||
removeClient={removeClient}
|
||||
/>
|
||||
) : (
|
||||
<CallingParticipantsList
|
||||
|
@ -388,6 +400,7 @@ function ActiveCallManager({
|
|||
hasRemoteVideo: hasLocalVideo,
|
||||
isHandRaised,
|
||||
presenting: Boolean(activeCall.presentingSource),
|
||||
demuxId: activeCall.localDemuxId,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
@ -396,12 +409,15 @@ function ActiveCallManager({
|
|||
<>
|
||||
<CallScreen
|
||||
activeCall={activeCall}
|
||||
approveUser={approveUser}
|
||||
changeCallView={changeCallView}
|
||||
denyUser={denyUser}
|
||||
getPresentingSources={getPresentingSources}
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSourceForActiveCall}
|
||||
groupMembers={groupMembers}
|
||||
hangUpActiveCall={hangUpActiveCall}
|
||||
i18n={i18n}
|
||||
isCallLinkAdmin={isCallLinkAdmin}
|
||||
isGroupCallRaiseHandEnabled={isGroupCallRaiseHandEnabled}
|
||||
me={me}
|
||||
openSystemPreferencesAction={openSystemPreferencesAction}
|
||||
|
@ -438,10 +454,12 @@ function ActiveCallManager({
|
|||
<CallingAdhocCallInfo
|
||||
callLink={callLink}
|
||||
i18n={i18n}
|
||||
isCallLinkAdmin={isCallLinkAdmin}
|
||||
ourServiceId={me.serviceId}
|
||||
participants={groupCallParticipantsForParticipantsList}
|
||||
onClose={toggleParticipants}
|
||||
onCopyCallLink={onCopyCallLink}
|
||||
removeClient={removeClient}
|
||||
/>
|
||||
) : (
|
||||
<CallingParticipantsList
|
||||
|
@ -458,6 +476,7 @@ function ActiveCallManager({
|
|||
export function CallManager({
|
||||
acceptCall,
|
||||
activeCall,
|
||||
approveUser,
|
||||
availableCameras,
|
||||
bounceAppIconStart,
|
||||
bounceAppIconStop,
|
||||
|
@ -466,6 +485,7 @@ export function CallManager({
|
|||
changeCallView,
|
||||
closeNeedPermissionScreen,
|
||||
declineCall,
|
||||
denyUser,
|
||||
getGroupCallVideoFrameSource,
|
||||
getPresentingSources,
|
||||
hangUpActiveCall,
|
||||
|
@ -479,6 +499,7 @@ export function CallManager({
|
|||
openSystemPreferencesAction,
|
||||
pauseVoiceNotePlayer,
|
||||
playRingtone,
|
||||
removeClient,
|
||||
renderDeviceSelection,
|
||||
renderEmojiPicker,
|
||||
renderReactionPicker,
|
||||
|
@ -552,10 +573,12 @@ export function CallManager({
|
|||
<ActiveCallManager
|
||||
activeCall={activeCall}
|
||||
availableCameras={availableCameras}
|
||||
approveUser={approveUser}
|
||||
callLink={callLink}
|
||||
cancelCall={cancelCall}
|
||||
changeCallView={changeCallView}
|
||||
closeNeedPermissionScreen={closeNeedPermissionScreen}
|
||||
denyUser={denyUser}
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||
getPresentingSources={getPresentingSources}
|
||||
hangUpActiveCall={hangUpActiveCall}
|
||||
|
@ -564,6 +587,7 @@ export function CallManager({
|
|||
me={me}
|
||||
openSystemPreferencesAction={openSystemPreferencesAction}
|
||||
pauseVoiceNotePlayer={pauseVoiceNotePlayer}
|
||||
removeClient={removeClient}
|
||||
renderDeviceSelection={renderDeviceSelection}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
renderReactionPicker={renderReactionPicker}
|
||||
|
|
|
@ -67,6 +67,7 @@ type GroupCallOverrideProps = OverridePropsBase & {
|
|||
callMode: CallMode.Group;
|
||||
connectionState?: GroupCallConnectionState;
|
||||
peekedParticipants?: Array<ConversationType>;
|
||||
pendingParticipants?: Array<ConversationType>;
|
||||
raisedHands?: Set<number>;
|
||||
remoteParticipants?: Array<GroupCallRemoteParticipantType>;
|
||||
remoteAudioLevel?: number;
|
||||
|
@ -135,6 +136,7 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
|
|||
isConversationTooBigToRing: false,
|
||||
peekedParticipants:
|
||||
overrideProps.peekedParticipants || overrideProps.remoteParticipants || [],
|
||||
pendingParticipants: overrideProps.pendingParticipants || [],
|
||||
raisedHands:
|
||||
overrideProps.raisedHands ||
|
||||
getRaisedHands(overrideProps) ||
|
||||
|
@ -181,11 +183,14 @@ const createProps = (
|
|||
}
|
||||
): PropsType => ({
|
||||
activeCall: createActiveCallProp(overrideProps),
|
||||
approveUser: action('approve-user'),
|
||||
changeCallView: action('change-call-view'),
|
||||
denyUser: action('deny-user'),
|
||||
getGroupCallVideoFrameSource: fakeGetGroupCallVideoFrameSource,
|
||||
getPresentingSources: action('get-presenting-sources'),
|
||||
hangUpActiveCall: action('hang-up'),
|
||||
i18n,
|
||||
isCallLinkAdmin: true,
|
||||
isGroupCallRaiseHandEnabled: true,
|
||||
me: getDefaultConversation({
|
||||
color: AvatarColors[1],
|
||||
|
|
|
@ -8,6 +8,7 @@ import classNames from 'classnames';
|
|||
import type { VideoFrameSource } from '@signalapp/ringrtc';
|
||||
import type {
|
||||
ActiveCallStateType,
|
||||
PendingUserActionPayloadType,
|
||||
SendGroupCallRaiseHandType,
|
||||
SendGroupCallReactionType,
|
||||
SetLocalAudioType,
|
||||
|
@ -88,14 +89,18 @@ import {
|
|||
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall';
|
||||
import { assertDev } from '../util/assert';
|
||||
import { emojiToData } from './emoji/lib';
|
||||
import { CallingPendingParticipants } from './CallingPendingParticipants';
|
||||
|
||||
export type PropsType = {
|
||||
activeCall: ActiveCallType;
|
||||
approveUser: (payload: PendingUserActionPayloadType) => void;
|
||||
denyUser: (payload: PendingUserActionPayloadType) => void;
|
||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||
getPresentingSources: () => void;
|
||||
groupMembers?: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
|
||||
hangUpActiveCall: (reason: string) => void;
|
||||
i18n: LocalizerType;
|
||||
isCallLinkAdmin: boolean;
|
||||
isGroupCallRaiseHandEnabled: boolean;
|
||||
me: ConversationType;
|
||||
openSystemPreferencesAction: () => unknown;
|
||||
|
@ -178,12 +183,15 @@ function CallDuration({
|
|||
|
||||
export function CallScreen({
|
||||
activeCall,
|
||||
approveUser,
|
||||
changeCallView,
|
||||
denyUser,
|
||||
getGroupCallVideoFrameSource,
|
||||
getPresentingSources,
|
||||
groupMembers,
|
||||
hangUpActiveCall,
|
||||
i18n,
|
||||
isCallLinkAdmin,
|
||||
isGroupCallRaiseHandEnabled,
|
||||
me,
|
||||
openSystemPreferencesAction,
|
||||
|
@ -396,6 +404,11 @@ export function CallScreen({
|
|||
throw missingCaseError(activeCall);
|
||||
}
|
||||
|
||||
const pendingParticipants =
|
||||
activeCall.callMode === CallMode.Adhoc && isCallLinkAdmin
|
||||
? activeCall.pendingParticipants
|
||||
: [];
|
||||
|
||||
let lonelyInCallNode: ReactNode;
|
||||
let localPreviewNode: ReactNode;
|
||||
|
||||
|
@ -811,6 +824,15 @@ export function CallScreen({
|
|||
renderRaisedHandsToast={renderRaisedHandsToast}
|
||||
i18n={i18n}
|
||||
/>
|
||||
{pendingParticipants.length ? (
|
||||
<CallingPendingParticipants
|
||||
i18n={i18n}
|
||||
ourServiceId={me.serviceId}
|
||||
participants={pendingParticipants}
|
||||
approveUser={approveUser}
|
||||
denyUser={denyUser}
|
||||
/>
|
||||
) : null}
|
||||
{/* We render the local preview first and set the footer flex direction to row-reverse
|
||||
to ensure the preview is visible at low viewport widths. */}
|
||||
<div className="module-ongoing-call__footer">
|
||||
|
|
|
@ -61,10 +61,12 @@ function getCallLink(overrideProps: Partial<CallLinkType> = {}): CallLinkType {
|
|||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
callLink: getCallLink(overrideProps.callLink || {}),
|
||||
i18n,
|
||||
isCallLinkAdmin: overrideProps.isCallLinkAdmin || false,
|
||||
ourServiceId: generateAci(),
|
||||
participants: overrideProps.participants || [],
|
||||
onClose: action('on-close'),
|
||||
onCopyCallLink: action('on-copy-call-link'),
|
||||
removeClient: overrideProps.removeClient || action('remove-client'),
|
||||
});
|
||||
|
||||
export default {
|
||||
|
|
|
@ -16,29 +16,35 @@ import { sortByTitle } from '../util/sortByTitle';
|
|||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { ModalHost } from './ModalHost';
|
||||
import { isInSystemContacts } from '../util/isInSystemContacts';
|
||||
import type { RemoveClientType } from '../state/ducks/calling';
|
||||
|
||||
type ParticipantType = ConversationType & {
|
||||
hasRemoteAudio?: boolean;
|
||||
hasRemoteVideo?: boolean;
|
||||
isHandRaised?: boolean;
|
||||
presenting?: boolean;
|
||||
demuxId?: number;
|
||||
};
|
||||
|
||||
export type PropsType = {
|
||||
readonly callLink: CallLinkType;
|
||||
readonly i18n: LocalizerType;
|
||||
readonly isCallLinkAdmin: boolean;
|
||||
readonly ourServiceId: ServiceIdString | undefined;
|
||||
readonly participants: Array<ParticipantType>;
|
||||
readonly onClose: () => void;
|
||||
readonly onCopyCallLink: () => void;
|
||||
readonly removeClient: ((payload: RemoveClientType) => void) | null;
|
||||
};
|
||||
|
||||
export function CallingAdhocCallInfo({
|
||||
i18n,
|
||||
isCallLinkAdmin,
|
||||
ourServiceId,
|
||||
participants,
|
||||
onClose,
|
||||
onCopyCallLink,
|
||||
removeClient,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const sortedParticipants = React.useMemo<Array<ParticipantType>>(
|
||||
() => sortByTitle(participants),
|
||||
|
@ -137,6 +143,26 @@ export function CallingAdhocCallInfo({
|
|||
'module-calling-participants-list__muted--audio'
|
||||
)}
|
||||
/>
|
||||
{isCallLinkAdmin &&
|
||||
removeClient &&
|
||||
participant.demuxId &&
|
||||
!(ourServiceId && participant.serviceId === ourServiceId) ? (
|
||||
<button
|
||||
aria-label={i18n('icu:CallingAdhocCallInfo__RemoveClient')}
|
||||
className={classNames(
|
||||
'CallingAdhocCallInfo__RemoveClient',
|
||||
'module-calling-participants-list__status-icon',
|
||||
'module-calling-participants-list__remove'
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!participant.demuxId) {
|
||||
return;
|
||||
}
|
||||
removeClient({ demuxId: participant.demuxId });
|
||||
}}
|
||||
type="button"
|
||||
/>
|
||||
) : null}
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
|
|
101
ts/components/CallingPendingParticipants.tsx
Normal file
101
ts/components/CallingPendingParticipants.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
|
||||
import React from 'react';
|
||||
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 { Button, ButtonVariant } from './Button';
|
||||
|
||||
export type PropsType = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly ourServiceId: ServiceIdString | undefined;
|
||||
readonly participants: Array<ConversationType>;
|
||||
readonly approveUser: (payload: PendingUserActionPayloadType) => void;
|
||||
readonly denyUser: (payload: PendingUserActionPayloadType) => void;
|
||||
};
|
||||
|
||||
export function CallingPendingParticipants({
|
||||
i18n,
|
||||
ourServiceId,
|
||||
participants,
|
||||
approveUser,
|
||||
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,
|
||||
})}
|
||||
</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}
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
|
@ -139,6 +139,7 @@ export function GroupCall(args: PropsType): JSX.Element {
|
|||
maxDevices: 5,
|
||||
deviceCount: 0,
|
||||
peekedParticipants: [],
|
||||
pendingParticipants: [],
|
||||
raisedHands: new Set<number>(),
|
||||
remoteParticipants: [],
|
||||
remoteAudioLevels: new Map<number, number>(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue