Improve display of unknown contacts in call links
This commit is contained in:
parent
7e31b37417
commit
33ec40d7b4
10 changed files with 506 additions and 146 deletions
|
@ -3686,6 +3686,26 @@
|
|||
"messageformat": "Remove this person from the call",
|
||||
"description": "Button in the in-call info popup for call link calls showing all participants. The action is to remove the participant from the call."
|
||||
},
|
||||
"icu:CallingAdhocCallInfo__UnknownContactLabel": {
|
||||
"messageformat": "{count, plural, one {# person} other {# people}}",
|
||||
"description": "Label showing number of unknown contacts in the in-call participant info popup for call links."
|
||||
},
|
||||
"icu:CallingAdhocCallInfo__UnknownContactLabel--in-addition": {
|
||||
"messageformat": "+{count, number} more",
|
||||
"description": "Label showing number of unknown contacts in the in-call participant info popup for call links, when known contacts are also present in the call."
|
||||
},
|
||||
"icu:CallingAdhocCallInfo__UnknownContactInfoButton": {
|
||||
"messageformat": "More info about new contacts",
|
||||
"description": "Aria label for info button in the in-call participant info popup for call links when unknown contacts are in the call."
|
||||
},
|
||||
"icu:CallingAdhocCallInfo__UnknownContactInfoDialogBody": {
|
||||
"messageformat": "Before joining a call you can only see the names of phone contacts, people you’re in a group with, or people you’ve chatted with 1:1. You’ll see all names and photos once you’ve joined the call.",
|
||||
"description": "Text for an info dialog which can be opened from the in-call participant list, which is available in call links when unknown contacts are in the call"
|
||||
},
|
||||
"icu:CallingAdhocCallInfo__UnknownContactInfoDialogOk": {
|
||||
"messageformat": "Got it",
|
||||
"description": "Button text for info dialog which can be opened from the in-call participant list, which is available in call links when unknown contacts are in the call."
|
||||
},
|
||||
"icu:callingDeviceSelection__label--video": {
|
||||
"messageformat": "Video",
|
||||
"description": "Label for video input selector"
|
||||
|
|
|
@ -52,10 +52,10 @@
|
|||
.CallingAdhocCallInfo__MenuItemIcon {
|
||||
background: $color-gray-65;
|
||||
display: flex;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-inline-end: 8px;
|
||||
border-radius: 32px;
|
||||
border-radius: 36px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
@ -93,3 +93,47 @@
|
|||
margin-inline: 8px;
|
||||
background: $color-white;
|
||||
}
|
||||
|
||||
.CallingAdhocCallInfo__UnknownContactInfoButton {
|
||||
@include button-reset;
|
||||
@include color-svg('../images/icons/v3/info/info.svg', $color-white);
|
||||
display: flex;
|
||||
flex: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-inline: 8px;
|
||||
}
|
||||
|
||||
.CallingAdhocCallInfo__UnknownContactInfoButton:focus {
|
||||
@include keyboard-mode {
|
||||
background: $color-ultramarine;
|
||||
}
|
||||
}
|
||||
|
||||
.CallingAdhocCallInfo__UnknownContactInfoDialog__body {
|
||||
padding-block-start: 22px;
|
||||
padding-block-end: 8px;
|
||||
}
|
||||
|
||||
.CallingAdhocCallInfo__UnknownContactAvatarSet {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.CallingAdhocCallInfo__UnknownContactAvatar {
|
||||
&:not(:first-child) {
|
||||
margin-inline-start: -24px;
|
||||
}
|
||||
|
||||
.module-Avatar__contents {
|
||||
outline: 2px solid;
|
||||
outline-color: $color-gray-80;
|
||||
}
|
||||
}
|
||||
|
||||
.CallingAdhocCallInfo
|
||||
.module-calling-participants-list__contact:hover
|
||||
.CallingAdhocCallInfo__UnknownContactAvatar
|
||||
.module-Avatar__contents {
|
||||
// Should match background of .module-calling-participants-list__contact:hover
|
||||
outline-color: $color-gray-62;
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ export enum AvatarSize {
|
|||
TWENTY = 20,
|
||||
TWENTY_FOUR = 24,
|
||||
TWENTY_EIGHT = 28,
|
||||
THIRTY = 30,
|
||||
THIRTY_TWO = 32,
|
||||
THIRTY_SIX = 36,
|
||||
FORTY = 40,
|
||||
|
@ -86,6 +87,7 @@ export type Props = {
|
|||
|
||||
const BADGE_PLACEMENT_BY_SIZE = new Map<number, BadgePlacementType>([
|
||||
[28, { bottom: -4, right: -2 }],
|
||||
[30, { bottom: -4, right: -2 }],
|
||||
[32, { bottom: -4, right: -2 }],
|
||||
[36, { bottom: -3, right: 0 }],
|
||||
[40, { bottom: -6, right: -4 }],
|
||||
|
@ -159,7 +161,10 @@ export function Avatar({
|
|||
const initials = getInitials(title);
|
||||
const hasImage = !noteToSelf && avatarPath && !imageBroken;
|
||||
const shouldUseInitials =
|
||||
!hasImage && conversationType === 'direct' && Boolean(initials);
|
||||
!hasImage &&
|
||||
conversationType === 'direct' &&
|
||||
Boolean(initials) &&
|
||||
title !== i18n('icu:unknownContact');
|
||||
|
||||
let contentsChildren: ReactNode;
|
||||
if (loading) {
|
||||
|
|
|
@ -7,6 +7,7 @@ import type { Meta } from '@storybook/react';
|
|||
import type { PropsType } from './CallManager';
|
||||
import { CallManager } from './CallManager';
|
||||
import {
|
||||
type ActiveGroupCallType,
|
||||
CallEndedReason,
|
||||
CallMode,
|
||||
CallState,
|
||||
|
@ -25,6 +26,12 @@ import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGr
|
|||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { StorySendMode } from '../types/Stories';
|
||||
import {
|
||||
FAKE_CALL_LINK,
|
||||
getDefaultCallLinkConversation,
|
||||
} from '../test-both/helpers/fakeCallLink';
|
||||
import { allRemoteParticipants } from './CallScreen.stories';
|
||||
import { getPlaceholderContact } from '../state/selectors/conversations';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -42,6 +49,11 @@ const getConversation = () =>
|
|||
lastUpdated: Date.now(),
|
||||
});
|
||||
|
||||
const getUnknownContact = (): ConversationType => ({
|
||||
...getPlaceholderContact(),
|
||||
serviceId: generateAci(),
|
||||
});
|
||||
|
||||
const getCommonActiveCallData = () => ({
|
||||
conversation: getConversation(),
|
||||
joinedAt: Date.now(),
|
||||
|
@ -69,12 +81,13 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
|||
denyUser: action('deny-user'),
|
||||
getGroupCallVideoFrameSource: (_: string, demuxId: number) =>
|
||||
fakeGetGroupCallVideoFrameSource(demuxId),
|
||||
getIsSharingPhoneNumberWithEverybody: () => false,
|
||||
getPresentingSources: action('get-presenting-sources'),
|
||||
hangUpActiveCall: action('hang-up-active-call'),
|
||||
hasInitialLoadCompleted: true,
|
||||
i18n,
|
||||
incomingCall: null,
|
||||
callLink: undefined,
|
||||
callLink: storyProps.callLink ?? undefined,
|
||||
me: {
|
||||
...getDefaultConversation({
|
||||
color: AvatarColors[0],
|
||||
|
@ -113,6 +126,38 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
|||
pauseVoiceNotePlayer: action('pause-audio-player'),
|
||||
});
|
||||
|
||||
const getActiveCallForCallLink = (
|
||||
overrideProps: Partial<ActiveGroupCallType> = {}
|
||||
): ActiveGroupCallType => {
|
||||
return {
|
||||
conversation: getDefaultCallLinkConversation(),
|
||||
joinedAt: Date.now(),
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: true,
|
||||
localAudioLevel: 0,
|
||||
viewMode: CallViewMode.Paginated,
|
||||
outgoingRing: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
showParticipantsList: overrideProps.showParticipantsList ?? true,
|
||||
callMode: CallMode.Adhoc,
|
||||
connectionState: GroupCallConnectionState.NotConnected,
|
||||
conversationsByDemuxId: new Map<number, ConversationType>(),
|
||||
deviceCount: 0,
|
||||
joinState: GroupCallJoinState.NotJoined,
|
||||
localDemuxId: 1,
|
||||
maxDevices: 5,
|
||||
groupMembers: [],
|
||||
isConversationTooBigToRing: false,
|
||||
peekedParticipants:
|
||||
overrideProps.peekedParticipants ?? allRemoteParticipants.slice(0, 3),
|
||||
remoteParticipants: overrideProps.remoteParticipants ?? [],
|
||||
pendingParticipants: [],
|
||||
raisedHands: new Set<number>(),
|
||||
remoteAudioLevels: new Map<number, number>(),
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/CallManager',
|
||||
argTypes: {},
|
||||
|
@ -226,3 +271,93 @@ export function CallRequestNeeded(): JSX.Element {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CallLinkLobbyParticipantsKnown(): JSX.Element {
|
||||
return (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: getActiveCallForCallLink(),
|
||||
callLink: FAKE_CALL_LINK,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CallLinkLobbyParticipants1Unknown(): JSX.Element {
|
||||
return (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: getActiveCallForCallLink({
|
||||
peekedParticipants: [getPlaceholderContact()],
|
||||
}),
|
||||
callLink: FAKE_CALL_LINK,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CallLinkLobbyParticipants1Known1Unknown(): JSX.Element {
|
||||
return (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: getActiveCallForCallLink({
|
||||
peekedParticipants: [allRemoteParticipants[0], getUnknownContact()],
|
||||
}),
|
||||
callLink: FAKE_CALL_LINK,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CallLinkLobbyParticipants1Known2Unknown(): JSX.Element {
|
||||
return (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: getActiveCallForCallLink({
|
||||
peekedParticipants: [
|
||||
getUnknownContact(),
|
||||
allRemoteParticipants[0],
|
||||
getUnknownContact(),
|
||||
],
|
||||
}),
|
||||
callLink: FAKE_CALL_LINK,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CallLinkLobbyParticipants1Known12Unknown(): JSX.Element {
|
||||
const peekedParticipants: Array<ConversationType> = [
|
||||
allRemoteParticipants[0],
|
||||
];
|
||||
for (let n = 12; n > 0; n -= 1) {
|
||||
peekedParticipants.push(getUnknownContact());
|
||||
}
|
||||
return (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: getActiveCallForCallLink({
|
||||
peekedParticipants,
|
||||
}),
|
||||
callLink: FAKE_CALL_LINK,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CallLinkLobbyParticipants3Unknown(): JSX.Element {
|
||||
return (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: getActiveCallForCallLink({
|
||||
peekedParticipants: [
|
||||
getUnknownContact(),
|
||||
getUnknownContact(),
|
||||
getUnknownContact(),
|
||||
],
|
||||
}),
|
||||
callLink: FAKE_CALL_LINK,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -53,7 +53,6 @@ import * as log from '../logging/log';
|
|||
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall';
|
||||
import { CallingAdhocCallInfo } from './CallingAdhocCallInfo';
|
||||
import { callLinkRootKeyToUrl } from '../util/callLinkRootKeyToUrl';
|
||||
import { isSharingPhoneNumberWithEverybody } from '../util/phoneNumberSharingMode';
|
||||
import { usePrevious } from '../hooks/usePrevious';
|
||||
import { copyCallLink } from '../util/copyLinksWithToast';
|
||||
|
||||
|
@ -90,6 +89,7 @@ export type PropsType = {
|
|||
conversationId: string,
|
||||
demuxId: number
|
||||
) => VideoFrameSource;
|
||||
getIsSharingPhoneNumberWithEverybody: () => boolean;
|
||||
getPresentingSources: () => void;
|
||||
incomingCall: DirectIncomingCall | GroupIncomingCall | null;
|
||||
renderDeviceSelection: () => JSX.Element;
|
||||
|
@ -146,7 +146,6 @@ type ActiveCallManagerPropsType = {
|
|||
| 'declineCall'
|
||||
| 'hasInitialLoadCompleted'
|
||||
| 'incomingCall'
|
||||
| 'isConversationTooBigToRin'
|
||||
| 'notifyForCall'
|
||||
| 'playRingtone'
|
||||
| 'setIsCallActive'
|
||||
|
@ -165,6 +164,7 @@ function ActiveCallManager({
|
|||
denyUser,
|
||||
hangUpActiveCall,
|
||||
i18n,
|
||||
getIsSharingPhoneNumberWithEverybody,
|
||||
getGroupCallVideoFrameSource,
|
||||
getPresentingSources,
|
||||
me,
|
||||
|
@ -349,7 +349,9 @@ function ActiveCallManager({
|
|||
isAdhocJoinRequestPending={isAdhocJoinRequestPending}
|
||||
isCallFull={isCallFull}
|
||||
isConversationTooBigToRing={isConvoTooBigToRing}
|
||||
isSharingPhoneNumberWithEverybody={isSharingPhoneNumberWithEverybody()}
|
||||
getIsSharingPhoneNumberWithEverybody={
|
||||
getIsSharingPhoneNumberWithEverybody
|
||||
}
|
||||
me={me}
|
||||
onCallCanceled={cancelActiveCall}
|
||||
onJoinCall={joinActiveCall}
|
||||
|
@ -501,6 +503,7 @@ export function CallManager({
|
|||
i18n,
|
||||
incomingCall,
|
||||
isConversationTooBigToRing,
|
||||
getIsSharingPhoneNumberWithEverybody,
|
||||
me,
|
||||
notifyForCall,
|
||||
openSystemPreferencesAction,
|
||||
|
@ -589,6 +592,9 @@ export function CallManager({
|
|||
getPresentingSources={getPresentingSources}
|
||||
hangUpActiveCall={hangUpActiveCall}
|
||||
i18n={i18n}
|
||||
getIsSharingPhoneNumberWithEverybody={
|
||||
getIsSharingPhoneNumberWithEverybody
|
||||
}
|
||||
me={me}
|
||||
openSystemPreferencesAction={openSystemPreferencesAction}
|
||||
pauseVoiceNotePlayer={pauseVoiceNotePlayer}
|
||||
|
|
|
@ -235,6 +235,7 @@ export default {
|
|||
title: 'Components/CallScreen',
|
||||
argTypes: {},
|
||||
args: {},
|
||||
excludeStories: ['allRemoteParticipants'],
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
export function Default(): JSX.Element {
|
||||
|
@ -378,7 +379,7 @@ export function GroupCallYourHandRaised(): JSX.Element {
|
|||
const PARTICIPANT_EMOJIS = ['❤️', '🤔', '✨', '😂', '🦄'] as const;
|
||||
|
||||
// We generate these upfront so that the list is stable when you move the slider.
|
||||
const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => {
|
||||
export const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => {
|
||||
const mediaKeysReceived = (index + 1) % 20 !== 0;
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { partition } from 'lodash';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import { ContactName } from './conversation/ContactName';
|
||||
import { InContactsIcon } from './InContactsIcon';
|
||||
|
@ -17,6 +16,12 @@ import type { ConversationType } from '../state/ducks/conversations';
|
|||
import { ModalHost } from './ModalHost';
|
||||
import { isInSystemContacts } from '../util/isInSystemContacts';
|
||||
import type { RemoveClientType } from '../state/ducks/calling';
|
||||
import { AvatarColors } from '../types/Colors';
|
||||
import { Button } from './Button';
|
||||
import { Modal } from './Modal';
|
||||
import { Theme } from '../util/theme';
|
||||
|
||||
const MAX_UNKNOWN_AVATARS_COUNT = 3;
|
||||
|
||||
type ParticipantType = ConversationType & {
|
||||
hasRemoteAudio?: boolean;
|
||||
|
@ -37,6 +42,99 @@ export type PropsType = {
|
|||
readonly removeClient: ((payload: RemoveClientType) => void) | null;
|
||||
};
|
||||
|
||||
type UnknownContactsPropsType = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly isInAdditionToKnownContacts: boolean;
|
||||
readonly participants: Array<ParticipantType>;
|
||||
readonly showUnknownContactDialog: () => void;
|
||||
};
|
||||
|
||||
function UnknownContacts({
|
||||
i18n,
|
||||
isInAdditionToKnownContacts,
|
||||
participants,
|
||||
showUnknownContactDialog,
|
||||
}: UnknownContactsPropsType): JSX.Element {
|
||||
const renderUnknownAvatar = React.useCallback(
|
||||
({
|
||||
participant,
|
||||
key,
|
||||
size,
|
||||
}: {
|
||||
participant: ParticipantType;
|
||||
key: React.Key;
|
||||
size: AvatarSize;
|
||||
}) => {
|
||||
const colorIndex = participant.serviceId
|
||||
? (parseInt(participant.serviceId.slice(-4), 16) || 0) %
|
||||
AvatarColors.length
|
||||
: 0;
|
||||
return (
|
||||
<Avatar
|
||||
acceptedMessageRequest={participant.acceptedMessageRequest}
|
||||
avatarPath={participant.avatarPath}
|
||||
badge={undefined}
|
||||
className="CallingAdhocCallInfo__UnknownContactAvatar"
|
||||
color={AvatarColors[colorIndex]}
|
||||
conversationType="direct"
|
||||
key={key}
|
||||
i18n={i18n}
|
||||
isMe={participant.isMe}
|
||||
profileName={participant.profileName}
|
||||
title={participant.title}
|
||||
sharedGroupNames={participant.sharedGroupNames}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[i18n]
|
||||
);
|
||||
|
||||
const visibleParticipants = participants.slice(0, MAX_UNKNOWN_AVATARS_COUNT);
|
||||
let avatarSize: AvatarSize;
|
||||
if (visibleParticipants.length === 1) {
|
||||
avatarSize = AvatarSize.THIRTY_SIX;
|
||||
} else if (visibleParticipants.length === 2) {
|
||||
avatarSize = AvatarSize.THIRTY;
|
||||
} else {
|
||||
avatarSize = AvatarSize.TWENTY_EIGHT;
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className="module-calling-participants-list__contact"
|
||||
key="unknown-contacts"
|
||||
>
|
||||
<div className="module-calling-participants-list__avatar-and-name">
|
||||
<div
|
||||
className={classNames(
|
||||
'CallingAdhocCallInfo__UnknownContactAvatarSet',
|
||||
'module-calling-participants-list__avatar-and-name'
|
||||
)}
|
||||
>
|
||||
{visibleParticipants.map((participant, key) =>
|
||||
renderUnknownAvatar({ participant, key, size: avatarSize })
|
||||
)}
|
||||
<div className="module-contact-name module-calling-participants-list__name">
|
||||
{i18n(
|
||||
isInAdditionToKnownContacts
|
||||
? 'icu:CallingAdhocCallInfo__UnknownContactLabel--in-addition'
|
||||
: 'icu:CallingAdhocCallInfo__UnknownContactLabel',
|
||||
{ count: participants.length }
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
aria-label="icu:CallingAdhocCallInfo__UnknownContactInfoButton"
|
||||
className="CallingAdhocCallInfo__UnknownContactInfoButton module-calling-participants-list__status-icon module-calling-participants-list__unknown-contact"
|
||||
onClick={showUnknownContactDialog}
|
||||
type="button"
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export function CallingAdhocCallInfo({
|
||||
i18n,
|
||||
isCallLinkAdmin,
|
||||
|
@ -46,141 +144,188 @@ export function CallingAdhocCallInfo({
|
|||
onCopyCallLink,
|
||||
removeClient,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const sortedParticipants = React.useMemo<Array<ParticipantType>>(
|
||||
() => sortByTitle(participants),
|
||||
const [isUnknownContactDialogVisible, setIsUnknownContactDialogVisible] =
|
||||
React.useState(false);
|
||||
const hideUnknownContactDialog = React.useCallback(
|
||||
() => setIsUnknownContactDialogVisible(false),
|
||||
[setIsUnknownContactDialogVisible]
|
||||
);
|
||||
|
||||
const [knownParticipants, unknownParticipants] = React.useMemo<
|
||||
[Array<ParticipantType>, Array<ParticipantType>]
|
||||
>(
|
||||
() =>
|
||||
partition(participants, (participant: ParticipantType) =>
|
||||
Boolean(participant.titleNoDefault)
|
||||
),
|
||||
[participants]
|
||||
);
|
||||
const sortedParticipants = React.useMemo<Array<ParticipantType>>(
|
||||
() => sortByTitle(knownParticipants),
|
||||
[knownParticipants]
|
||||
);
|
||||
|
||||
const renderParticipant = React.useCallback(
|
||||
(participant: ParticipantType, key: React.Key) => (
|
||||
<li
|
||||
className="module-calling-participants-list__contact"
|
||||
// It's tempting to use `participant.serviceId` as the `key`
|
||||
// here, but that can result in duplicate keys for
|
||||
// participants who have joined on multiple devices.
|
||||
key={key}
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
{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>
|
||||
<span
|
||||
className={classNames(
|
||||
'module-calling-participants-list__status-icon',
|
||||
participant.isHandRaised &&
|
||||
'module-calling-participants-list__hand-raised'
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={classNames(
|
||||
'module-calling-participants-list__status-icon',
|
||||
participant.presenting &&
|
||||
'module-calling-participants-list__presenting',
|
||||
!participant.hasRemoteVideo &&
|
||||
'module-calling-participants-list__muted--video'
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={classNames(
|
||||
'module-calling-participants-list__status-icon',
|
||||
!participant.hasRemoteAudio &&
|
||||
'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>
|
||||
),
|
||||
[i18n, isCallLinkAdmin, ourServiceId, removeClient]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalHost
|
||||
modalName="CallingAdhocCallInfo"
|
||||
moduleClassName="CallingAdhocCallInfo"
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="CallingAdhocCallInfo module-calling-participants-list">
|
||||
<div className="module-calling-participants-list__header">
|
||||
<div className="module-calling-participants-list__title">
|
||||
{participants.length
|
||||
? i18n('icu:calling__in-this-call', {
|
||||
people: participants.length,
|
||||
})
|
||||
: i18n('icu:calling__in-this-call--zero')}
|
||||
<>
|
||||
{isUnknownContactDialogVisible ? (
|
||||
<Modal
|
||||
modalName="CallingAdhocCallInfo.UnknownContactInfo"
|
||||
moduleClassName="CallingAdhocCallInfo__UnknownContactInfoDialog"
|
||||
i18n={i18n}
|
||||
modalFooter={
|
||||
<Button onClick={hideUnknownContactDialog}>
|
||||
{i18n('icu:CallingAdhocCallInfo__UnknownContactInfoDialogOk')}
|
||||
</Button>
|
||||
}
|
||||
onClose={hideUnknownContactDialog}
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
{i18n('icu:CallingAdhocCallInfo__UnknownContactInfoDialogBody')}
|
||||
</Modal>
|
||||
) : null}
|
||||
<ModalHost
|
||||
modalName="CallingAdhocCallInfo"
|
||||
moduleClassName="CallingAdhocCallInfo"
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="CallingAdhocCallInfo module-calling-participants-list">
|
||||
<div className="module-calling-participants-list__header">
|
||||
<div className="module-calling-participants-list__title">
|
||||
{participants.length
|
||||
? i18n('icu:calling__in-this-call', {
|
||||
people: participants.length,
|
||||
})
|
||||
: i18n('icu:calling__in-this-call--zero')}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="module-calling-participants-list__close"
|
||||
onClick={onClose}
|
||||
tabIndex={0}
|
||||
aria-label={i18n('icu:close')}
|
||||
/>
|
||||
</div>
|
||||
<ul className="module-calling-participants-list__list">
|
||||
{sortedParticipants.map(renderParticipant)}
|
||||
{unknownParticipants.length > 0 && (
|
||||
<UnknownContacts
|
||||
i18n={i18n}
|
||||
isInAdditionToKnownContacts={Boolean(knownParticipants.length)}
|
||||
participants={unknownParticipants}
|
||||
showUnknownContactDialog={() =>
|
||||
setIsUnknownContactDialogVisible(true)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
<div className="CallingAdhocCallInfo__Divider" />
|
||||
<div className="CallingAdhocCallInfo__CallLinkInfo">
|
||||
<button
|
||||
className="CallingAdhocCallInfo__MenuItem"
|
||||
onClick={onCopyCallLink}
|
||||
type="button"
|
||||
>
|
||||
<span className="CallingAdhocCallInfo__MenuItemIcon CallingAdhocCallInfo__MenuItemIcon--copy-link" />
|
||||
<span className="CallingAdhocCallInfo__MenuItemText">
|
||||
{i18n('icu:CallingAdhocCallInfo__CopyLink')}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="module-calling-participants-list__close"
|
||||
onClick={onClose}
|
||||
tabIndex={0}
|
||||
aria-label={i18n('icu:close')}
|
||||
/>
|
||||
</div>
|
||||
<ul className="module-calling-participants-list__list">
|
||||
{sortedParticipants.map(
|
||||
(participant: ParticipantType, index: number) => (
|
||||
<li
|
||||
className="module-calling-participants-list__contact"
|
||||
// It's tempting to use `participant.serviceId` as the `key`
|
||||
// here, but that can result in duplicate keys for
|
||||
// participants who have joined on multiple devices.
|
||||
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>
|
||||
<span
|
||||
className={classNames(
|
||||
'module-calling-participants-list__status-icon',
|
||||
participant.isHandRaised &&
|
||||
'module-calling-participants-list__hand-raised'
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={classNames(
|
||||
'module-calling-participants-list__status-icon',
|
||||
participant.presenting &&
|
||||
'module-calling-participants-list__presenting',
|
||||
!participant.hasRemoteVideo &&
|
||||
'module-calling-participants-list__muted--video'
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={classNames(
|
||||
'module-calling-participants-list__status-icon',
|
||||
!participant.hasRemoteAudio &&
|
||||
'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>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
<div className="CallingAdhocCallInfo__Divider" />
|
||||
<div className="CallingAdhocCallInfo__CallLinkInfo">
|
||||
<button
|
||||
className="CallingAdhocCallInfo__MenuItem"
|
||||
onClick={onCopyCallLink}
|
||||
type="button"
|
||||
>
|
||||
<span className="CallingAdhocCallInfo__MenuItemIcon CallingAdhocCallInfo__MenuItemIcon--copy-link" />
|
||||
<span className="CallingAdhocCallInfo__MenuItemText">
|
||||
{i18n('icu:CallingAdhocCallInfo__CopyLink')}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalHost>
|
||||
</ModalHost>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -70,8 +70,8 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
|
|||
isAdhocJoinRequestPending: overrideProps.isAdhocJoinRequestPending ?? false,
|
||||
isConversationTooBigToRing: false,
|
||||
isCallFull: overrideProps.isCallFull ?? false,
|
||||
isSharingPhoneNumberWithEverybody:
|
||||
overrideProps.isSharingPhoneNumberWithEverybody ?? false,
|
||||
getIsSharingPhoneNumberWithEverybody:
|
||||
overrideProps.getIsSharingPhoneNumberWithEverybody ?? (() => false),
|
||||
me:
|
||||
overrideProps.me ||
|
||||
getDefaultConversation({
|
||||
|
|
|
@ -51,6 +51,7 @@ export type PropsType = {
|
|||
| 'type'
|
||||
| 'unblurredAvatarPath'
|
||||
>;
|
||||
getIsSharingPhoneNumberWithEverybody: () => boolean;
|
||||
groupMembers?: Array<
|
||||
Pick<
|
||||
ConversationType,
|
||||
|
@ -64,7 +65,6 @@ export type PropsType = {
|
|||
isAdhocJoinRequestPending: boolean;
|
||||
isConversationTooBigToRing: boolean;
|
||||
isCallFull?: boolean;
|
||||
isSharingPhoneNumberWithEverybody: boolean;
|
||||
me: Readonly<
|
||||
Pick<ConversationType, 'avatarPath' | 'color' | 'id' | 'serviceId'>
|
||||
>;
|
||||
|
@ -94,7 +94,7 @@ export function CallingLobby({
|
|||
isAdhocJoinRequestPending,
|
||||
isCallFull = false,
|
||||
isConversationTooBigToRing,
|
||||
isSharingPhoneNumberWithEverybody,
|
||||
getIsSharingPhoneNumberWithEverybody,
|
||||
me,
|
||||
onCallCanceled,
|
||||
onJoinCall,
|
||||
|
@ -333,7 +333,7 @@ export function CallingLobby({
|
|||
</div>
|
||||
) : (
|
||||
<div className="CallingLobby__CallLinkNotice">
|
||||
{isSharingPhoneNumberWithEverybody
|
||||
{getIsSharingPhoneNumberWithEverybody()
|
||||
? i18n('icu:CallingLobby__CallLinkNotice--phone-sharing')
|
||||
: i18n('icu:CallingLobby__CallLinkNotice')}
|
||||
</div>
|
||||
|
|
|
@ -53,6 +53,7 @@ import { getIntl } from '../selectors/user';
|
|||
import { SmartCallingDeviceSelection } from './CallingDeviceSelection';
|
||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||
import { renderReactionPicker } from './renderReactionPicker';
|
||||
import { isSharingPhoneNumberWithEverybody as getIsSharingPhoneNumberWithEverybody } from '../../util/phoneNumberSharingMode';
|
||||
|
||||
function renderDeviceSelection(): JSX.Element {
|
||||
return <SmartCallingDeviceSelection />;
|
||||
|
@ -468,6 +469,9 @@ export const SmartCallManager = memo(function SmartCallManager() {
|
|||
declineCall={declineCall}
|
||||
denyUser={denyUser}
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||
getIsSharingPhoneNumberWithEverybody={
|
||||
getIsSharingPhoneNumberWithEverybody
|
||||
}
|
||||
getPresentingSources={getPresentingSources}
|
||||
hangUpActiveCall={hangUpActiveCall}
|
||||
hasInitialLoadCompleted={hasInitialLoadCompleted}
|
||||
|
|
Loading…
Reference in a new issue