Improve display of unknown contacts in call links

Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com>
This commit is contained in:
automated-signal 2024-06-10 16:55:35 -05:00 committed by GitHub
parent 98ea022822
commit f79a3b1aab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 506 additions and 146 deletions

View file

@ -3686,6 +3686,26 @@
"messageformat": "Remove this person from the call", "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." "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 youre in a group with, or people youve chatted with 1:1. Youll see all names and photos once youve 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": { "icu:callingDeviceSelection__label--video": {
"messageformat": "Video", "messageformat": "Video",
"description": "Label for video input selector" "description": "Label for video input selector"

View file

@ -52,10 +52,10 @@
.CallingAdhocCallInfo__MenuItemIcon { .CallingAdhocCallInfo__MenuItemIcon {
background: $color-gray-65; background: $color-gray-65;
display: flex; display: flex;
width: 32px; width: 36px;
height: 32px; height: 36px;
margin-inline-end: 8px; margin-inline-end: 8px;
border-radius: 32px; border-radius: 36px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
@ -93,3 +93,47 @@
margin-inline: 8px; margin-inline: 8px;
background: $color-white; 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;
}

View file

@ -38,6 +38,7 @@ export enum AvatarSize {
TWENTY = 20, TWENTY = 20,
TWENTY_FOUR = 24, TWENTY_FOUR = 24,
TWENTY_EIGHT = 28, TWENTY_EIGHT = 28,
THIRTY = 30,
THIRTY_TWO = 32, THIRTY_TWO = 32,
THIRTY_SIX = 36, THIRTY_SIX = 36,
FORTY = 40, FORTY = 40,
@ -86,6 +87,7 @@ export type Props = {
const BADGE_PLACEMENT_BY_SIZE = new Map<number, BadgePlacementType>([ const BADGE_PLACEMENT_BY_SIZE = new Map<number, BadgePlacementType>([
[28, { bottom: -4, right: -2 }], [28, { bottom: -4, right: -2 }],
[30, { bottom: -4, right: -2 }],
[32, { bottom: -4, right: -2 }], [32, { bottom: -4, right: -2 }],
[36, { bottom: -3, right: 0 }], [36, { bottom: -3, right: 0 }],
[40, { bottom: -6, right: -4 }], [40, { bottom: -6, right: -4 }],
@ -159,7 +161,10 @@ export function Avatar({
const initials = getInitials(title); const initials = getInitials(title);
const hasImage = !noteToSelf && avatarPath && !imageBroken; const hasImage = !noteToSelf && avatarPath && !imageBroken;
const shouldUseInitials = const shouldUseInitials =
!hasImage && conversationType === 'direct' && Boolean(initials); !hasImage &&
conversationType === 'direct' &&
Boolean(initials) &&
title !== i18n('icu:unknownContact');
let contentsChildren: ReactNode; let contentsChildren: ReactNode;
if (loading) { if (loading) {

View file

@ -7,6 +7,7 @@ import type { Meta } from '@storybook/react';
import type { PropsType } from './CallManager'; import type { PropsType } from './CallManager';
import { CallManager } from './CallManager'; import { CallManager } from './CallManager';
import { import {
type ActiveGroupCallType,
CallEndedReason, CallEndedReason,
CallMode, CallMode,
CallState, CallState,
@ -25,6 +26,12 @@ import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGr
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { StorySendMode } from '../types/Stories'; 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); const i18n = setupI18n('en', enMessages);
@ -42,6 +49,11 @@ const getConversation = () =>
lastUpdated: Date.now(), lastUpdated: Date.now(),
}); });
const getUnknownContact = (): ConversationType => ({
...getPlaceholderContact(),
serviceId: generateAci(),
});
const getCommonActiveCallData = () => ({ const getCommonActiveCallData = () => ({
conversation: getConversation(), conversation: getConversation(),
joinedAt: Date.now(), joinedAt: Date.now(),
@ -69,12 +81,13 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
denyUser: action('deny-user'), denyUser: action('deny-user'),
getGroupCallVideoFrameSource: (_: string, demuxId: number) => getGroupCallVideoFrameSource: (_: string, demuxId: number) =>
fakeGetGroupCallVideoFrameSource(demuxId), fakeGetGroupCallVideoFrameSource(demuxId),
getIsSharingPhoneNumberWithEverybody: () => false,
getPresentingSources: action('get-presenting-sources'), getPresentingSources: action('get-presenting-sources'),
hangUpActiveCall: action('hang-up-active-call'), hangUpActiveCall: action('hang-up-active-call'),
hasInitialLoadCompleted: true, hasInitialLoadCompleted: true,
i18n, i18n,
incomingCall: null, incomingCall: null,
callLink: undefined, callLink: storyProps.callLink ?? undefined,
me: { me: {
...getDefaultConversation({ ...getDefaultConversation({
color: AvatarColors[0], color: AvatarColors[0],
@ -113,6 +126,38 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
pauseVoiceNotePlayer: action('pause-audio-player'), 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 { export default {
title: 'Components/CallManager', title: 'Components/CallManager',
argTypes: {}, 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,
})}
/>
);
}

View file

@ -53,7 +53,6 @@ import * as log from '../logging/log';
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall'; import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall';
import { CallingAdhocCallInfo } from './CallingAdhocCallInfo'; import { CallingAdhocCallInfo } from './CallingAdhocCallInfo';
import { callLinkRootKeyToUrl } from '../util/callLinkRootKeyToUrl'; import { callLinkRootKeyToUrl } from '../util/callLinkRootKeyToUrl';
import { isSharingPhoneNumberWithEverybody } from '../util/phoneNumberSharingMode';
import { usePrevious } from '../hooks/usePrevious'; import { usePrevious } from '../hooks/usePrevious';
import { copyCallLink } from '../util/copyLinksWithToast'; import { copyCallLink } from '../util/copyLinksWithToast';
@ -90,6 +89,7 @@ export type PropsType = {
conversationId: string, conversationId: string,
demuxId: number demuxId: number
) => VideoFrameSource; ) => VideoFrameSource;
getIsSharingPhoneNumberWithEverybody: () => boolean;
getPresentingSources: () => void; getPresentingSources: () => void;
incomingCall: DirectIncomingCall | GroupIncomingCall | null; incomingCall: DirectIncomingCall | GroupIncomingCall | null;
renderDeviceSelection: () => JSX.Element; renderDeviceSelection: () => JSX.Element;
@ -146,7 +146,6 @@ type ActiveCallManagerPropsType = {
| 'declineCall' | 'declineCall'
| 'hasInitialLoadCompleted' | 'hasInitialLoadCompleted'
| 'incomingCall' | 'incomingCall'
| 'isConversationTooBigToRin'
| 'notifyForCall' | 'notifyForCall'
| 'playRingtone' | 'playRingtone'
| 'setIsCallActive' | 'setIsCallActive'
@ -165,6 +164,7 @@ function ActiveCallManager({
denyUser, denyUser,
hangUpActiveCall, hangUpActiveCall,
i18n, i18n,
getIsSharingPhoneNumberWithEverybody,
getGroupCallVideoFrameSource, getGroupCallVideoFrameSource,
getPresentingSources, getPresentingSources,
me, me,
@ -349,7 +349,9 @@ function ActiveCallManager({
isAdhocJoinRequestPending={isAdhocJoinRequestPending} isAdhocJoinRequestPending={isAdhocJoinRequestPending}
isCallFull={isCallFull} isCallFull={isCallFull}
isConversationTooBigToRing={isConvoTooBigToRing} isConversationTooBigToRing={isConvoTooBigToRing}
isSharingPhoneNumberWithEverybody={isSharingPhoneNumberWithEverybody()} getIsSharingPhoneNumberWithEverybody={
getIsSharingPhoneNumberWithEverybody
}
me={me} me={me}
onCallCanceled={cancelActiveCall} onCallCanceled={cancelActiveCall}
onJoinCall={joinActiveCall} onJoinCall={joinActiveCall}
@ -501,6 +503,7 @@ export function CallManager({
i18n, i18n,
incomingCall, incomingCall,
isConversationTooBigToRing, isConversationTooBigToRing,
getIsSharingPhoneNumberWithEverybody,
me, me,
notifyForCall, notifyForCall,
openSystemPreferencesAction, openSystemPreferencesAction,
@ -589,6 +592,9 @@ export function CallManager({
getPresentingSources={getPresentingSources} getPresentingSources={getPresentingSources}
hangUpActiveCall={hangUpActiveCall} hangUpActiveCall={hangUpActiveCall}
i18n={i18n} i18n={i18n}
getIsSharingPhoneNumberWithEverybody={
getIsSharingPhoneNumberWithEverybody
}
me={me} me={me}
openSystemPreferencesAction={openSystemPreferencesAction} openSystemPreferencesAction={openSystemPreferencesAction}
pauseVoiceNotePlayer={pauseVoiceNotePlayer} pauseVoiceNotePlayer={pauseVoiceNotePlayer}

View file

@ -235,6 +235,7 @@ export default {
title: 'Components/CallScreen', title: 'Components/CallScreen',
argTypes: {}, argTypes: {},
args: {}, args: {},
excludeStories: ['allRemoteParticipants'],
} satisfies Meta<PropsType>; } satisfies Meta<PropsType>;
export function Default(): JSX.Element { export function Default(): JSX.Element {
@ -378,7 +379,7 @@ export function GroupCallYourHandRaised(): JSX.Element {
const PARTICIPANT_EMOJIS = ['❤️', '🤔', '✨', '😂', '🦄'] as const; const PARTICIPANT_EMOJIS = ['❤️', '🤔', '✨', '😂', '🦄'] as const;
// We generate these upfront so that the list is stable when you move the slider. // 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; const mediaKeysReceived = (index + 1) % 20 !== 0;
return { return {

View file

@ -1,11 +1,10 @@
// Copyright 2024 Signal Messenger, LLC // Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable react/no-array-index-key */
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { partition } from 'lodash';
import { Avatar, AvatarSize } from './Avatar'; import { Avatar, AvatarSize } from './Avatar';
import { ContactName } from './conversation/ContactName'; import { ContactName } from './conversation/ContactName';
import { InContactsIcon } from './InContactsIcon'; import { InContactsIcon } from './InContactsIcon';
@ -17,6 +16,12 @@ import type { ConversationType } from '../state/ducks/conversations';
import { ModalHost } from './ModalHost'; import { ModalHost } from './ModalHost';
import { isInSystemContacts } from '../util/isInSystemContacts'; import { isInSystemContacts } from '../util/isInSystemContacts';
import type { RemoveClientType } from '../state/ducks/calling'; 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 & { type ParticipantType = ConversationType & {
hasRemoteAudio?: boolean; hasRemoteAudio?: boolean;
@ -37,6 +42,99 @@ export type PropsType = {
readonly removeClient: ((payload: RemoveClientType) => void) | null; 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({ export function CallingAdhocCallInfo({
i18n, i18n,
isCallLinkAdmin, isCallLinkAdmin,
@ -46,141 +144,188 @@ export function CallingAdhocCallInfo({
onCopyCallLink, onCopyCallLink,
removeClient, removeClient,
}: PropsType): JSX.Element | null { }: PropsType): JSX.Element | null {
const sortedParticipants = React.useMemo<Array<ParticipantType>>( const [isUnknownContactDialogVisible, setIsUnknownContactDialogVisible] =
() => sortByTitle(participants), 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] [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 ( return (
<ModalHost <>
modalName="CallingAdhocCallInfo" {isUnknownContactDialogVisible ? (
moduleClassName="CallingAdhocCallInfo" <Modal
onClose={onClose} modalName="CallingAdhocCallInfo.UnknownContactInfo"
> moduleClassName="CallingAdhocCallInfo__UnknownContactInfoDialog"
<div className="CallingAdhocCallInfo module-calling-participants-list"> i18n={i18n}
<div className="module-calling-participants-list__header"> modalFooter={
<div className="module-calling-participants-list__title"> <Button onClick={hideUnknownContactDialog}>
{participants.length {i18n('icu:CallingAdhocCallInfo__UnknownContactInfoDialogOk')}
? i18n('icu:calling__in-this-call', { </Button>
people: participants.length, }
}) onClose={hideUnknownContactDialog}
: i18n('icu:calling__in-this-call--zero')} 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> </div>
<button
type="button"
className="module-calling-participants-list__close"
onClick={onClose}
tabIndex={0}
aria-label={i18n('icu:close')}
/>
</div> </div>
<ul className="module-calling-participants-list__list"> </ModalHost>
{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>
); );
} }

View file

@ -70,8 +70,8 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
isAdhocJoinRequestPending: overrideProps.isAdhocJoinRequestPending ?? false, isAdhocJoinRequestPending: overrideProps.isAdhocJoinRequestPending ?? false,
isConversationTooBigToRing: false, isConversationTooBigToRing: false,
isCallFull: overrideProps.isCallFull ?? false, isCallFull: overrideProps.isCallFull ?? false,
isSharingPhoneNumberWithEverybody: getIsSharingPhoneNumberWithEverybody:
overrideProps.isSharingPhoneNumberWithEverybody ?? false, overrideProps.getIsSharingPhoneNumberWithEverybody ?? (() => false),
me: me:
overrideProps.me || overrideProps.me ||
getDefaultConversation({ getDefaultConversation({

View file

@ -51,6 +51,7 @@ export type PropsType = {
| 'type' | 'type'
| 'unblurredAvatarPath' | 'unblurredAvatarPath'
>; >;
getIsSharingPhoneNumberWithEverybody: () => boolean;
groupMembers?: Array< groupMembers?: Array<
Pick< Pick<
ConversationType, ConversationType,
@ -64,7 +65,6 @@ export type PropsType = {
isAdhocJoinRequestPending: boolean; isAdhocJoinRequestPending: boolean;
isConversationTooBigToRing: boolean; isConversationTooBigToRing: boolean;
isCallFull?: boolean; isCallFull?: boolean;
isSharingPhoneNumberWithEverybody: boolean;
me: Readonly< me: Readonly<
Pick<ConversationType, 'avatarPath' | 'color' | 'id' | 'serviceId'> Pick<ConversationType, 'avatarPath' | 'color' | 'id' | 'serviceId'>
>; >;
@ -94,7 +94,7 @@ export function CallingLobby({
isAdhocJoinRequestPending, isAdhocJoinRequestPending,
isCallFull = false, isCallFull = false,
isConversationTooBigToRing, isConversationTooBigToRing,
isSharingPhoneNumberWithEverybody, getIsSharingPhoneNumberWithEverybody,
me, me,
onCallCanceled, onCallCanceled,
onJoinCall, onJoinCall,
@ -333,7 +333,7 @@ export function CallingLobby({
</div> </div>
) : ( ) : (
<div className="CallingLobby__CallLinkNotice"> <div className="CallingLobby__CallLinkNotice">
{isSharingPhoneNumberWithEverybody {getIsSharingPhoneNumberWithEverybody()
? i18n('icu:CallingLobby__CallLinkNotice--phone-sharing') ? i18n('icu:CallingLobby__CallLinkNotice--phone-sharing')
: i18n('icu:CallingLobby__CallLinkNotice')} : i18n('icu:CallingLobby__CallLinkNotice')}
</div> </div>

View file

@ -53,6 +53,7 @@ import { getIntl } from '../selectors/user';
import { SmartCallingDeviceSelection } from './CallingDeviceSelection'; import { SmartCallingDeviceSelection } from './CallingDeviceSelection';
import { renderEmojiPicker } from './renderEmojiPicker'; import { renderEmojiPicker } from './renderEmojiPicker';
import { renderReactionPicker } from './renderReactionPicker'; import { renderReactionPicker } from './renderReactionPicker';
import { isSharingPhoneNumberWithEverybody as getIsSharingPhoneNumberWithEverybody } from '../../util/phoneNumberSharingMode';
function renderDeviceSelection(): JSX.Element { function renderDeviceSelection(): JSX.Element {
return <SmartCallingDeviceSelection />; return <SmartCallingDeviceSelection />;
@ -468,6 +469,9 @@ export const SmartCallManager = memo(function SmartCallManager() {
declineCall={declineCall} declineCall={declineCall}
denyUser={denyUser} denyUser={denyUser}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource} getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
getIsSharingPhoneNumberWithEverybody={
getIsSharingPhoneNumberWithEverybody
}
getPresentingSources={getPresentingSources} getPresentingSources={getPresentingSources}
hangUpActiveCall={hangUpActiveCall} hangUpActiveCall={hangUpActiveCall}
hasInitialLoadCompleted={hasInitialLoadCompleted} hasInitialLoadCompleted={hasInitialLoadCompleted}