Handles safety number changes while in a call
This commit is contained in:
parent
561baf6309
commit
318013e83d
26 changed files with 387 additions and 162 deletions
|
@ -17,7 +17,9 @@ import {
|
|||
} from '../types/Calling';
|
||||
import { ConversationTypeType } from '../state/ducks/conversations';
|
||||
import { Colors, ColorType } from '../types/Colors';
|
||||
import { getDefaultConversation } from '../util/getDefaultConversation';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import { Props as SafetyNumberViewerProps } from '../state/smart/SafetyNumberViewer';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
@ -68,12 +70,16 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
|||
getGroupCallVideoFrameSource: noop as any,
|
||||
hangUp: action('hang-up'),
|
||||
i18n,
|
||||
keyChangeOk: action('key-change-ok'),
|
||||
me: {
|
||||
...getDefaultConversation({
|
||||
color: select('Caller color', Colors, 'ultramarine' as ColorType),
|
||||
title: text('Caller Title', 'Morty Smith'),
|
||||
}),
|
||||
uuid: 'cb0dd0c8-7393-41e9-a0aa-d631c4109541',
|
||||
color: select('Caller color', Colors, 'ultramarine' as ColorType),
|
||||
title: text('Caller Title', 'Morty Smith'),
|
||||
},
|
||||
renderDeviceSelection: () => <div />,
|
||||
renderSafetyNumberViewer: (_: SafetyNumberViewerProps) => <div />,
|
||||
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
||||
setLocalAudio: action('set-local-audio'),
|
||||
setLocalPreview: action('set-local-preview'),
|
||||
|
@ -110,6 +116,7 @@ story.add('Ongoing Group Call', () => (
|
|||
...getCommonActiveCallData(),
|
||||
callMode: CallMode.Group,
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
conversationsWithSafetyNumberChanges: [],
|
||||
deviceCount: 0,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
maxDevices: 5,
|
||||
|
@ -145,3 +152,27 @@ story.add('Call Request Needed', () => (
|
|||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Group call - Safety Number Changed', () => (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: {
|
||||
...getCommonActiveCallData(),
|
||||
callMode: CallMode.Group,
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
conversationsWithSafetyNumberChanges: [
|
||||
{
|
||||
...getDefaultConversation({
|
||||
title: 'Aaron',
|
||||
}),
|
||||
},
|
||||
],
|
||||
deviceCount: 0,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
maxDevices: 5,
|
||||
peekedParticipants: [],
|
||||
remoteParticipants: [],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -8,6 +8,10 @@ import { CallingLobby } from './CallingLobby';
|
|||
import { CallingParticipantsList } from './CallingParticipantsList';
|
||||
import { CallingPip } from './CallingPip';
|
||||
import { IncomingCallBar } from './IncomingCallBar';
|
||||
import {
|
||||
SafetyNumberChangeDialog,
|
||||
SafetyNumberProps,
|
||||
} from './SafetyNumberChangeDialog';
|
||||
import {
|
||||
ActiveCallType,
|
||||
CallEndedReason,
|
||||
|
@ -24,6 +28,7 @@ import {
|
|||
DeclineCallType,
|
||||
DirectCallStateType,
|
||||
HangUpType,
|
||||
KeyChangeOkType,
|
||||
SetGroupCallVideoRequestType,
|
||||
SetLocalAudioType,
|
||||
SetLocalPreviewType,
|
||||
|
@ -32,9 +37,12 @@ import {
|
|||
StartCallType,
|
||||
} from '../state/ducks/calling';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
|
||||
interface MeType extends ConversationType {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export interface PropsType {
|
||||
activeCall?: ActiveCallType;
|
||||
availableCameras: Array<MediaDeviceInfo>;
|
||||
|
@ -48,21 +56,15 @@ export interface PropsType {
|
|||
call: DirectCallStateType;
|
||||
conversation: ConversationType;
|
||||
};
|
||||
keyChangeOk: (_: KeyChangeOkType) => void;
|
||||
renderDeviceSelection: () => JSX.Element;
|
||||
renderSafetyNumberViewer: (props: SafetyNumberProps) => JSX.Element;
|
||||
startCall: (payload: StartCallType) => void;
|
||||
toggleParticipants: () => void;
|
||||
acceptCall: (_: AcceptCallType) => void;
|
||||
declineCall: (_: DeclineCallType) => void;
|
||||
i18n: LocalizerType;
|
||||
me: {
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
title: string;
|
||||
uuid: string;
|
||||
};
|
||||
me: MeType;
|
||||
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
|
||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||
|
@ -84,9 +86,11 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
closeNeedPermissionScreen,
|
||||
hangUp,
|
||||
i18n,
|
||||
keyChangeOk,
|
||||
getGroupCallVideoFrameSource,
|
||||
me,
|
||||
renderDeviceSelection,
|
||||
renderSafetyNumberViewer,
|
||||
setGroupCallVideoRequest,
|
||||
setLocalAudio,
|
||||
setLocalPreview,
|
||||
|
@ -203,6 +207,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
<CallingParticipantsList
|
||||
i18n={i18n}
|
||||
onClose={toggleParticipants}
|
||||
ourUuid={me.uuid}
|
||||
participants={peekedParticipants}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -233,13 +238,11 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
...participant,
|
||||
hasAudio: participant.hasRemoteAudio,
|
||||
hasVideo: participant.hasRemoteVideo,
|
||||
isSelf: false,
|
||||
})),
|
||||
{
|
||||
...me,
|
||||
hasAudio: hasLocalAudio,
|
||||
hasVideo: hasLocalVideo,
|
||||
isSelf: true,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
@ -268,9 +271,25 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
<CallingParticipantsList
|
||||
i18n={i18n}
|
||||
onClose={toggleParticipants}
|
||||
ourUuid={me.uuid}
|
||||
participants={groupCallParticipantsForParticipantsList}
|
||||
/>
|
||||
) : null}
|
||||
{activeCall.callMode === CallMode.Group &&
|
||||
activeCall.conversationsWithSafetyNumberChanges.length ? (
|
||||
<SafetyNumberChangeDialog
|
||||
confirmText={i18n('continueCall')}
|
||||
contacts={activeCall.conversationsWithSafetyNumberChanges}
|
||||
i18n={i18n}
|
||||
onCancel={() => {
|
||||
hangUp({ conversationId: activeCall.conversation.id });
|
||||
}}
|
||||
onConfirm={() => {
|
||||
keyChangeOk({ conversationId: activeCall.conversation.id });
|
||||
}}
|
||||
renderSafetyNumber={renderSafetyNumberViewer}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,13 +12,14 @@ import {
|
|||
CallState,
|
||||
GroupCallConnectionState,
|
||||
GroupCallJoinState,
|
||||
GroupCallPeekedParticipantType,
|
||||
GroupCallRemoteParticipantType,
|
||||
} from '../types/Calling';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import { Colors } from '../types/Colors';
|
||||
import { CallScreen, PropsType } from './CallScreen';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { getDefaultConversation } from '../util/getDefaultConversation';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
@ -50,7 +51,7 @@ interface DirectCallOverrideProps extends OverridePropsBase {
|
|||
interface GroupCallOverrideProps extends OverridePropsBase {
|
||||
callMode: CallMode.Group;
|
||||
connectionState?: GroupCallConnectionState;
|
||||
peekedParticipants?: Array<GroupCallPeekedParticipantType>;
|
||||
peekedParticipants?: Array<ConversationType>;
|
||||
remoteParticipants?: Array<GroupCallRemoteParticipantType>;
|
||||
}
|
||||
|
||||
|
@ -83,6 +84,7 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
|
|||
callMode: CallMode.Group as CallMode.Group,
|
||||
connectionState:
|
||||
overrideProps.connectionState || GroupCallConnectionState.Connected,
|
||||
conversationsWithSafetyNumberChanges: [],
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
maxDevices: 5,
|
||||
deviceCount: (overrideProps.remoteParticipants || []).length,
|
||||
|
@ -240,14 +242,15 @@ story.add('Group call - 1', () => (
|
|||
callMode: CallMode.Group,
|
||||
remoteParticipants: [
|
||||
{
|
||||
uuid: '72fa60e5-25fb-472d-8a56-e56867c57dda',
|
||||
demuxId: 0,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isBlocked: false,
|
||||
isSelf: false,
|
||||
title: 'Tyler',
|
||||
videoAspectRatio: 1.3,
|
||||
...getDefaultConversation({
|
||||
isBlocked: false,
|
||||
uuid: '72fa60e5-25fb-472d-8a56-e56867c57dda',
|
||||
title: 'Tyler',
|
||||
}),
|
||||
},
|
||||
],
|
||||
})}
|
||||
|
@ -260,34 +263,37 @@ story.add('Group call - Many', () => (
|
|||
callMode: CallMode.Group,
|
||||
remoteParticipants: [
|
||||
{
|
||||
uuid: '094586f5-8fc2-4ce2-a152-2dfcc99f4630',
|
||||
demuxId: 0,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isBlocked: false,
|
||||
isSelf: false,
|
||||
title: 'Amy',
|
||||
videoAspectRatio: 1.3,
|
||||
...getDefaultConversation({
|
||||
isBlocked: false,
|
||||
title: 'Amy',
|
||||
uuid: '094586f5-8fc2-4ce2-a152-2dfcc99f4630',
|
||||
}),
|
||||
},
|
||||
{
|
||||
uuid: 'cb5bdb24-4cbb-4650-8a7a-1a2807051e74',
|
||||
demuxId: 1,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isBlocked: false,
|
||||
isSelf: true,
|
||||
title: 'Bob',
|
||||
videoAspectRatio: 1.3,
|
||||
...getDefaultConversation({
|
||||
isBlocked: false,
|
||||
title: 'Bob',
|
||||
uuid: 'cb5bdb24-4cbb-4650-8a7a-1a2807051e74',
|
||||
}),
|
||||
},
|
||||
{
|
||||
uuid: '2d7d13ae-53dc-4a51-8dc7-976cd85e0b57',
|
||||
demuxId: 2,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isBlocked: true,
|
||||
isSelf: false,
|
||||
title: 'Alice',
|
||||
videoAspectRatio: 1.3,
|
||||
...getDefaultConversation({
|
||||
isBlocked: true,
|
||||
title: 'Alice',
|
||||
uuid: '2d7d13ae-53dc-4a51-8dc7-976cd85e0b57',
|
||||
}),
|
||||
},
|
||||
],
|
||||
})}
|
||||
|
@ -301,14 +307,15 @@ story.add('Group call - reconnecting', () => (
|
|||
connectionState: GroupCallConnectionState.Reconnecting,
|
||||
remoteParticipants: [
|
||||
{
|
||||
uuid: '33871c64-0c22-45ce-8aa4-0ec237ac4a31',
|
||||
demuxId: 0,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isBlocked: false,
|
||||
isSelf: false,
|
||||
title: 'Tyler',
|
||||
videoAspectRatio: 1.3,
|
||||
...getDefaultConversation({
|
||||
isBlocked: false,
|
||||
title: 'Tyler',
|
||||
uuid: '33871c64-0c22-45ce-8aa4-0ec237ac4a31',
|
||||
}),
|
||||
},
|
||||
],
|
||||
})}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { ColorType } from '../types/Colors';
|
|||
import { CallingLobby, PropsType } from './CallingLobby';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { getDefaultConversation } from '../util/getDefaultConversation';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -33,7 +34,10 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
|
||||
i18n,
|
||||
isGroupCall: boolean('isGroupCall', overrideProps.isGroupCall || false),
|
||||
me: overrideProps.me || { color: 'ultramarine' as ColorType },
|
||||
me: overrideProps.me || {
|
||||
color: 'ultramarine' as ColorType,
|
||||
uuid: generateUuid(),
|
||||
},
|
||||
onCallCanceled: action('on-call-canceled'),
|
||||
onJoinCall: action('on-join-call'),
|
||||
peekedParticipants: overrideProps.peekedParticipants || [],
|
||||
|
@ -48,11 +52,11 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
toggleSettings: action('toggle-settings'),
|
||||
});
|
||||
|
||||
const fakePeekedParticipant = (title: string) => ({
|
||||
isSelf: false,
|
||||
title,
|
||||
uuid: generateUuid(),
|
||||
});
|
||||
const fakePeekedParticipant = (title: string) =>
|
||||
getDefaultConversation({
|
||||
title,
|
||||
uuid: generateUuid(),
|
||||
});
|
||||
|
||||
const story = storiesOf('Components/CallingLobby', module);
|
||||
|
||||
|
@ -72,8 +76,9 @@ story.add('No Camera, local avatar', () => {
|
|||
const props = createProps({
|
||||
availableCameras: [],
|
||||
me: {
|
||||
color: 'ultramarine' as ColorType,
|
||||
avatarPath: '/fixtures/kitten-4-112-112.jpg',
|
||||
color: 'ultramarine' as ColorType,
|
||||
uuid: generateUuid(),
|
||||
},
|
||||
});
|
||||
return <CallingLobby {...props} />;
|
||||
|
|
|
@ -14,6 +14,7 @@ import { CallingHeader } from './CallingHeader';
|
|||
import { Spinner } from './Spinner';
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
|
||||
export type PropsType = {
|
||||
availableCameras: Array<MediaDeviceInfo>;
|
||||
|
@ -28,15 +29,11 @@ export type PropsType = {
|
|||
me: {
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
uuid: string;
|
||||
};
|
||||
onCallCanceled: () => void;
|
||||
onJoinCall: () => void;
|
||||
peekedParticipants: Array<{
|
||||
firstName?: string;
|
||||
isSelf: boolean;
|
||||
title: string;
|
||||
uuid: string;
|
||||
}>;
|
||||
peekedParticipants: Array<ConversationType>;
|
||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||
|
@ -124,7 +121,7 @@ export const CallingLobby = ({
|
|||
// device.
|
||||
// TODO: Improve the "it's you" case; see DESKTOP-926.
|
||||
const participantNames = peekedParticipants.map(participant =>
|
||||
participant.isSelf
|
||||
participant.uuid === me.uuid
|
||||
? i18n('you')
|
||||
: participant.firstName || participant.title
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@ import { v4 as generateUuid } from 'uuid';
|
|||
import { CallingParticipantsList, PropsType } from './CallingParticipantsList';
|
||||
import { Colors } from '../types/Colors';
|
||||
import { GroupCallRemoteParticipantType } from '../types/Calling';
|
||||
import { getDefaultConversation } from '../util/getDefaultConversation';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
|
@ -19,24 +20,26 @@ function createParticipant(
|
|||
): GroupCallRemoteParticipantType {
|
||||
const randomColor = Math.floor(Math.random() * Colors.length - 1);
|
||||
return {
|
||||
avatarPath: participantProps.avatarPath,
|
||||
color: Colors[randomColor],
|
||||
demuxId: 2,
|
||||
hasRemoteAudio: Boolean(participantProps.hasRemoteAudio),
|
||||
hasRemoteVideo: Boolean(participantProps.hasRemoteVideo),
|
||||
isBlocked: Boolean(participantProps.isBlocked),
|
||||
isSelf: Boolean(participantProps.isSelf),
|
||||
name: participantProps.name,
|
||||
profileName: participantProps.title,
|
||||
title: String(participantProps.title),
|
||||
videoAspectRatio: 1.3,
|
||||
uuid: generateUuid(),
|
||||
...getDefaultConversation({
|
||||
avatarPath: participantProps.avatarPath,
|
||||
color: Colors[randomColor],
|
||||
isBlocked: Boolean(participantProps.isBlocked),
|
||||
name: participantProps.name,
|
||||
profileName: participantProps.title,
|
||||
title: String(participantProps.title),
|
||||
uuid: generateUuid(),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
i18n,
|
||||
onClose: action('on-close'),
|
||||
ourUuid: 'cf085e6a-e70b-41ec-a310-c198248af13f',
|
||||
participants: overrideProps.participants || [],
|
||||
});
|
||||
|
||||
|
@ -62,7 +65,6 @@ story.add('Many Participants', () => {
|
|||
const props = createProps({
|
||||
participants: [
|
||||
createParticipant({
|
||||
isSelf: true,
|
||||
title: 'Son Goku',
|
||||
}),
|
||||
createParticipant({
|
||||
|
|
|
@ -9,10 +9,10 @@ import { Avatar } from './Avatar';
|
|||
import { ContactName } from './conversation/ContactName';
|
||||
import { InContactsIcon } from './InContactsIcon';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { GroupCallPeekedParticipantType } from '../types/Calling';
|
||||
import { sortByTitle } from '../util/sortByTitle';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
|
||||
interface ParticipantType extends GroupCallPeekedParticipantType {
|
||||
interface ParticipantType extends ConversationType {
|
||||
hasAudio?: boolean;
|
||||
hasVideo?: boolean;
|
||||
}
|
||||
|
@ -20,11 +20,12 @@ interface ParticipantType extends GroupCallPeekedParticipantType {
|
|||
export type PropsType = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly onClose: () => void;
|
||||
readonly ourUuid: string;
|
||||
readonly participants: Array<ParticipantType>;
|
||||
};
|
||||
|
||||
export const CallingParticipantsList = React.memo(
|
||||
({ i18n, onClose, participants }: PropsType) => {
|
||||
({ i18n, onClose, ourUuid, participants }: PropsType) => {
|
||||
const [root, setRoot] = React.useState<HTMLElement | null>(null);
|
||||
|
||||
const sortedParticipants = React.useMemo<Array<ParticipantType>>(
|
||||
|
@ -100,7 +101,7 @@ export const CallingParticipantsList = React.memo(
|
|||
title={participant.title}
|
||||
size={32}
|
||||
/>
|
||||
{participant.isSelf ? (
|
||||
{participant.uuid === ourUuid ? (
|
||||
<span className="module-calling-participants-list__name">
|
||||
{i18n('you')}
|
||||
</span>
|
||||
|
|
|
@ -105,6 +105,7 @@ story.add('Group Call', () => {
|
|||
...getCommonActiveCallData(),
|
||||
callMode: CallMode.Group as CallMode.Group,
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
conversationsWithSafetyNumberChanges: [],
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
maxDevices: 5,
|
||||
deviceCount: 0,
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
GroupCallRemoteParticipant,
|
||||
PropsType,
|
||||
} from './GroupCallRemoteParticipant';
|
||||
import { getDefaultConversation } from '../util/getDefaultConversation';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
|
@ -37,12 +38,13 @@ const createProps = (
|
|||
demuxId: 123,
|
||||
hasRemoteAudio: false,
|
||||
hasRemoteVideo: true,
|
||||
isBlocked: Boolean(isBlocked),
|
||||
isSelf: false,
|
||||
title:
|
||||
'Pablo Diego José Francisco de Paula Juan Nepomuceno María de los Remedios Cipriano de la Santísima Trinidad Ruiz y Picasso',
|
||||
videoAspectRatio: 1.3,
|
||||
uuid: '992ed3b9-fc9b-47a9-bdb4-e0c7cbb0fda5',
|
||||
...getDefaultConversation({
|
||||
isBlocked: Boolean(isBlocked),
|
||||
title:
|
||||
'Pablo Diego José Francisco de Paula Juan Nepomuceno María de los Remedios Cipriano de la Santísima Trinidad Ruiz y Picasso',
|
||||
uuid: '992ed3b9-fc9b-47a9-bdb4-e0c7cbb0fda5',
|
||||
}),
|
||||
},
|
||||
...overrideProps,
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ import { InContactsIcon } from './InContactsIcon';
|
|||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
type SafetyNumberProps = {
|
||||
export type SafetyNumberProps = {
|
||||
contactID: string;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue