Contact info modal for call link join requests
This commit is contained in:
parent
390eab2556
commit
84896d0fbb
19 changed files with 519 additions and 6 deletions
69
ts/components/CallLinkPendingParticipantModal.stories.tsx
Normal file
69
ts/components/CallLinkPendingParticipantModal.stories.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import type { CallLinkPendingParticipantModalProps } from './CallLinkPendingParticipantModal';
|
||||
import { CallLinkPendingParticipantModal } from './CallLinkPendingParticipantModal';
|
||||
import type { ComponentMeta } from '../storybook/types';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const conversation = getDefaultConversation({
|
||||
acceptedMessageRequest: true,
|
||||
hasMessages: true,
|
||||
});
|
||||
const conversationWithSharedGroups = getDefaultConversation({
|
||||
acceptedMessageRequest: true,
|
||||
aboutText: 'likes to chat',
|
||||
hasMessages: true,
|
||||
sharedGroupNames: ['Axolotl lovers'],
|
||||
});
|
||||
const systemContact = getDefaultConversation({
|
||||
acceptedMessageRequest: true,
|
||||
systemGivenName: 'Alice',
|
||||
phoneNumber: '+1 555 123-4567',
|
||||
hasMessages: true,
|
||||
});
|
||||
|
||||
export default {
|
||||
title: 'Components/CallLinkPendingParticipantModal',
|
||||
component: CallLinkPendingParticipantModal,
|
||||
args: {
|
||||
i18n,
|
||||
conversation,
|
||||
approveUser: action('approveUser'),
|
||||
denyUser: action('denyUser'),
|
||||
toggleAboutContactModal: action('toggleAboutContactModal'),
|
||||
onClose: action('onClose'),
|
||||
updateSharedGroups: action('updateSharedGroups'),
|
||||
},
|
||||
} satisfies ComponentMeta<CallLinkPendingParticipantModalProps>;
|
||||
|
||||
export function Default(
|
||||
args: CallLinkPendingParticipantModalProps
|
||||
): JSX.Element {
|
||||
return <CallLinkPendingParticipantModal {...args} />;
|
||||
}
|
||||
|
||||
export function SystemContact(
|
||||
args: CallLinkPendingParticipantModalProps
|
||||
): JSX.Element {
|
||||
return (
|
||||
<CallLinkPendingParticipantModal {...args} conversation={systemContact} />
|
||||
);
|
||||
}
|
||||
|
||||
export function WithSharedGroups(
|
||||
args: CallLinkPendingParticipantModalProps
|
||||
): JSX.Element {
|
||||
return (
|
||||
<CallLinkPendingParticipantModal
|
||||
{...args}
|
||||
conversation={conversationWithSharedGroups}
|
||||
/>
|
||||
);
|
||||
}
|
140
ts/components/CallLinkPendingParticipantModal.tsx
Normal file
140
ts/components/CallLinkPendingParticipantModal.tsx
Normal file
|
@ -0,0 +1,140 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Modal } from './Modal';
|
||||
import type { LocalizerType } from '../types/I18N';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import type { PendingUserActionPayloadType } from '../state/ducks/calling';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { InContactsIcon } from './InContactsIcon';
|
||||
import { isInSystemContacts } from '../util/isInSystemContacts';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import { Theme } from '../util/theme';
|
||||
import { UserText } from './UserText';
|
||||
import { SharedGroupNames } from './SharedGroupNames';
|
||||
|
||||
export type CallLinkPendingParticipantModalProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly conversation: ConversationType;
|
||||
readonly approveUser: (payload: PendingUserActionPayloadType) => void;
|
||||
readonly denyUser: (payload: PendingUserActionPayloadType) => void;
|
||||
readonly onClose: () => void;
|
||||
readonly toggleAboutContactModal: (conversationId: string) => void;
|
||||
readonly updateSharedGroups: (conversationId: string) => void;
|
||||
};
|
||||
|
||||
export function CallLinkPendingParticipantModal({
|
||||
i18n,
|
||||
conversation,
|
||||
approveUser,
|
||||
denyUser,
|
||||
onClose,
|
||||
toggleAboutContactModal,
|
||||
updateSharedGroups,
|
||||
}: CallLinkPendingParticipantModalProps): JSX.Element {
|
||||
useEffect(() => {
|
||||
// Kick off the expensive hydration of the current sharedGroupNames
|
||||
updateSharedGroups(conversation.id);
|
||||
}, [conversation.id, updateSharedGroups]);
|
||||
|
||||
const serviceId = useMemo(() => {
|
||||
return conversation.serviceId;
|
||||
}, [conversation]);
|
||||
|
||||
const handleApprove = useCallback(() => {
|
||||
approveUser({ serviceId });
|
||||
onClose();
|
||||
}, [approveUser, onClose, serviceId]);
|
||||
|
||||
const handleDeny = useCallback(() => {
|
||||
denyUser({ serviceId });
|
||||
onClose();
|
||||
}, [denyUser, onClose, serviceId]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
modalName="CallLinkPendingParticipantModal"
|
||||
moduleClassName="CallLinkPendingParticipantModal"
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
<Avatar
|
||||
acceptedMessageRequest={conversation.acceptedMessageRequest}
|
||||
avatarUrl={conversation.avatarUrl}
|
||||
badge={undefined}
|
||||
color={conversation.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={conversation.isMe}
|
||||
profileName={conversation.profileName}
|
||||
sharedGroupNames={conversation.sharedGroupNames}
|
||||
size={AvatarSize.EIGHTY}
|
||||
title={conversation.title}
|
||||
theme={ThemeType.dark}
|
||||
unblurredAvatarUrl={conversation.unblurredAvatarUrl}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleAboutContactModal(conversation.id);
|
||||
}}
|
||||
className="CallLinkPendingParticipantModal__NameButton"
|
||||
>
|
||||
<div className="CallLinkPendingParticipantModal__Title">
|
||||
<UserText text={conversation.title} />
|
||||
{isInSystemContacts(conversation) && (
|
||||
<span>
|
||||
{' '}
|
||||
<InContactsIcon
|
||||
className="module-in-contacts-icon__icon CallLinkPendingParticipantModal__InContactsIcon"
|
||||
i18n={i18n}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span className="CallLinkPendingParticipantModal__AboutIcon" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="CallLinkPendingParticipantModal__SharedGroupInfo">
|
||||
{conversation.sharedGroupNames?.length ? (
|
||||
<SharedGroupNames
|
||||
i18n={i18n}
|
||||
sharedGroupNames={conversation.sharedGroupNames || []}
|
||||
/>
|
||||
) : (
|
||||
i18n('icu:no-groups-in-common-warning')
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="CallLinkPendingParticipantModal__Hr" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="CallLinkPendingParticipantModal__ActionButton"
|
||||
onClick={handleApprove}
|
||||
>
|
||||
<div className="CallLinkPendingParticipantModal__ButtonIcon">
|
||||
<div className="CallLinkPendingParticipantModal__ButtonIconContent CallLinkPendingParticipantModal__ButtonIconContent--approve" />
|
||||
</div>
|
||||
{i18n('icu:CallLinkPendingParticipantModal__ApproveButtonLabel')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="CallLinkPendingParticipantModal__ActionButton"
|
||||
onClick={handleDeny}
|
||||
>
|
||||
<div className="CallLinkPendingParticipantModal__ButtonIcon">
|
||||
<div className="CallLinkPendingParticipantModal__ButtonIconContent CallLinkPendingParticipantModal__ButtonIconContent--deny" />
|
||||
</div>
|
||||
{i18n('icu:CallLinkPendingParticipantModal__DenyButtonLabel')}
|
||||
</button>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -139,6 +139,9 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
|||
stopRingtone: action('stop-ringtone'),
|
||||
switchToPresentationView: action('switch-to-presentation-view'),
|
||||
switchFromPresentationView: action('switch-from-presentation-view'),
|
||||
toggleCallLinkPendingParticipantModal: action(
|
||||
'toggle-call-link-pending-participant-modal'
|
||||
),
|
||||
toggleParticipants: action('toggle-participants'),
|
||||
togglePip: action('toggle-pip'),
|
||||
toggleScreenRecordingPermissionsDialog: action(
|
||||
|
|
|
@ -139,6 +139,7 @@ export type PropsType = {
|
|||
switchFromPresentationView: () => void;
|
||||
hangUpActiveCall: (reason: string) => void;
|
||||
togglePip: () => void;
|
||||
toggleCallLinkPendingParticipantModal: (contactId: string) => void;
|
||||
toggleScreenRecordingPermissionsDialog: () => unknown;
|
||||
toggleSettings: () => void;
|
||||
isConversationTooBigToRing: boolean;
|
||||
|
@ -199,6 +200,7 @@ function ActiveCallManager({
|
|||
startCall,
|
||||
switchToPresentationView,
|
||||
switchFromPresentationView,
|
||||
toggleCallLinkPendingParticipantModal,
|
||||
toggleParticipants,
|
||||
togglePip,
|
||||
toggleScreenRecordingPermissionsDialog,
|
||||
|
@ -475,6 +477,9 @@ function ActiveCallManager({
|
|||
stickyControls={showParticipantsList}
|
||||
switchToPresentationView={switchToPresentationView}
|
||||
switchFromPresentationView={switchFromPresentationView}
|
||||
toggleCallLinkPendingParticipantModal={
|
||||
toggleCallLinkPendingParticipantModal
|
||||
}
|
||||
toggleScreenRecordingPermissionsDialog={
|
||||
toggleScreenRecordingPermissionsDialog
|
||||
}
|
||||
|
@ -571,6 +576,7 @@ export function CallManager({
|
|||
switchToPresentationView,
|
||||
toggleParticipants,
|
||||
togglePip,
|
||||
toggleCallLinkPendingParticipantModal,
|
||||
toggleScreenRecordingPermissionsDialog,
|
||||
toggleSettings,
|
||||
}: PropsType): JSX.Element | null {
|
||||
|
@ -661,6 +667,9 @@ export function CallManager({
|
|||
startCall={startCall}
|
||||
switchFromPresentationView={switchFromPresentationView}
|
||||
switchToPresentationView={switchToPresentationView}
|
||||
toggleCallLinkPendingParticipantModal={
|
||||
toggleCallLinkPendingParticipantModal
|
||||
}
|
||||
toggleParticipants={toggleParticipants}
|
||||
togglePip={togglePip}
|
||||
toggleScreenRecordingPermissionsDialog={
|
||||
|
|
|
@ -217,6 +217,9 @@ const createProps = (
|
|||
stickyControls: false,
|
||||
switchToPresentationView: action('switch-to-presentation-view'),
|
||||
switchFromPresentationView: action('switch-from-presentation-view'),
|
||||
toggleCallLinkPendingParticipantModal: action(
|
||||
'toggle-call-link-pending-participant-modal'
|
||||
),
|
||||
toggleParticipants: action('toggle-participants'),
|
||||
togglePip: action('toggle-pip'),
|
||||
toggleScreenRecordingPermissionsDialog: action(
|
||||
|
|
|
@ -125,6 +125,7 @@ export type PropsType = {
|
|||
stickyControls: boolean;
|
||||
switchToPresentationView: () => void;
|
||||
switchFromPresentationView: () => void;
|
||||
toggleCallLinkPendingParticipantModal: (contactId: string) => void;
|
||||
toggleParticipants: () => void;
|
||||
togglePip: () => void;
|
||||
toggleScreenRecordingPermissionsDialog: () => unknown;
|
||||
|
@ -214,6 +215,7 @@ export function CallScreen({
|
|||
stickyControls,
|
||||
switchToPresentationView,
|
||||
switchFromPresentationView,
|
||||
toggleCallLinkPendingParticipantModal,
|
||||
toggleParticipants,
|
||||
togglePip,
|
||||
toggleScreenRecordingPermissionsDialog,
|
||||
|
@ -864,6 +866,9 @@ export function CallScreen({
|
|||
approveUser={approveUser}
|
||||
batchUserAction={batchUserAction}
|
||||
denyUser={denyUser}
|
||||
toggleCallLinkPendingParticipantModal={
|
||||
toggleCallLinkPendingParticipantModal
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{/* We render the local preview first and set the footer flex direction to row-reverse
|
||||
|
|
|
@ -18,6 +18,9 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
|||
approveUser: action('approve-user'),
|
||||
batchUserAction: action('batch-user-action'),
|
||||
denyUser: action('deny-user'),
|
||||
toggleCallLinkPendingParticipantModal: action(
|
||||
'toggle-call-link-pending-participant-modal'
|
||||
),
|
||||
...storyProps,
|
||||
});
|
||||
|
||||
|
|
|
@ -35,6 +35,9 @@ export type PropsType = {
|
|||
readonly approveUser: (payload: PendingUserActionPayloadType) => void;
|
||||
readonly batchUserAction: (payload: BatchUserActionPayloadType) => void;
|
||||
readonly denyUser: (payload: PendingUserActionPayloadType) => void;
|
||||
readonly toggleCallLinkPendingParticipantModal: (
|
||||
conversationId: string
|
||||
) => void;
|
||||
};
|
||||
|
||||
export function CallingPendingParticipants({
|
||||
|
@ -44,6 +47,7 @@ export function CallingPendingParticipants({
|
|||
approveUser,
|
||||
batchUserAction,
|
||||
denyUser,
|
||||
toggleCallLinkPendingParticipantModal,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const [isExpanded, setIsExpanded] = useState(defaultIsExpanded ?? false);
|
||||
const [confirmDialogState, setConfirmDialogState] =
|
||||
|
@ -241,7 +245,15 @@ export function CallingPendingParticipants({
|
|||
className="module-calling-participants-list__contact"
|
||||
key={index}
|
||||
>
|
||||
<div className="module-calling-participants-list__avatar-and-name">
|
||||
<button
|
||||
type="button"
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleCallLinkPendingParticipantModal(participant.id);
|
||||
}}
|
||||
className="module-calling-participants-list__avatar-and-name CallingPendingParticipants__ParticipantButton"
|
||||
>
|
||||
<Avatar
|
||||
acceptedMessageRequest={participant.acceptedMessageRequest}
|
||||
avatarUrl={participant.avatarUrl}
|
||||
|
@ -268,7 +280,7 @@ export function CallingPendingParticipants({
|
|||
/>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</button>
|
||||
{renderApprovalButtons(participant)}
|
||||
</li>
|
||||
))}
|
||||
|
@ -303,7 +315,15 @@ export function CallingPendingParticipants({
|
|||
return (
|
||||
<div className="CallingPendingParticipants CallingPendingParticipants--Compact module-calling-participants-list">
|
||||
<div className="CallingPendingParticipants__CompactParticipant">
|
||||
<div className="module-calling-participants-list__avatar-and-name">
|
||||
<button
|
||||
type="button"
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleCallLinkPendingParticipantModal(participant.id);
|
||||
}}
|
||||
className="module-calling-participants-list__avatar-and-name CallingPendingParticipants__ParticipantButton"
|
||||
>
|
||||
<Avatar
|
||||
acceptedMessageRequest={participant.acceptedMessageRequest}
|
||||
avatarUrl={participant.avatarUrl}
|
||||
|
@ -326,12 +346,13 @@ export function CallingPendingParticipants({
|
|||
i18n={i18n}
|
||||
/>
|
||||
) : null}
|
||||
<span className="CallingPendingParticipants__ParticipantAboutIcon" />
|
||||
</div>
|
||||
<div className="CallingPendingParticipants__WouldLikeToJoin">
|
||||
{i18n('icu:CallingPendingParticipants__WouldLikeToJoin')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{renderApprovalButtons(participant)}
|
||||
</div>
|
||||
{participants.length > 1 && (
|
||||
|
|
|
@ -36,6 +36,9 @@ export type PropsType = {
|
|||
// CallLinkEditModal
|
||||
callLinkEditModalRoomId: string | null;
|
||||
renderCallLinkEditModal: () => JSX.Element;
|
||||
// CallLinkPendingParticipantModal
|
||||
callLinkPendingParticipantContactId: string | undefined;
|
||||
renderCallLinkPendingParticipantModal: () => JSX.Element;
|
||||
// ConfirmLeaveCallModal
|
||||
confirmLeaveCallModalState: StartCallData | null;
|
||||
renderConfirmLeaveCallModal: () => JSX.Element;
|
||||
|
@ -118,6 +121,9 @@ export function GlobalModalContainer({
|
|||
// CallLinkEditModal
|
||||
callLinkEditModalRoomId,
|
||||
renderCallLinkEditModal,
|
||||
// CallLinkPendingParticipantModal
|
||||
callLinkPendingParticipantContactId,
|
||||
renderCallLinkPendingParticipantModal,
|
||||
// ConfirmLeaveCallModal
|
||||
confirmLeaveCallModalState,
|
||||
renderConfirmLeaveCallModal,
|
||||
|
@ -268,6 +274,12 @@ export function GlobalModalContainer({
|
|||
return renderContactModal();
|
||||
}
|
||||
|
||||
// This needs to be after the about contact modal because the pending participant modal
|
||||
// opens the about contact modal
|
||||
if (callLinkPendingParticipantContactId) {
|
||||
return renderCallLinkPendingParticipantModal();
|
||||
}
|
||||
|
||||
if (isStoriesSettingsVisible) {
|
||||
return renderStoriesSettings();
|
||||
}
|
||||
|
|
|
@ -38,6 +38,12 @@ const conversationWithAbout = getDefaultConversation({
|
|||
aboutText: '😀 About Me',
|
||||
hasMessages: true,
|
||||
});
|
||||
const conversationWithSharedGroups = getDefaultConversation({
|
||||
acceptedMessageRequest: true,
|
||||
aboutText: 'likes to chat',
|
||||
hasMessages: true,
|
||||
sharedGroupNames: ['Axolotl lovers'],
|
||||
});
|
||||
const systemContact = getDefaultConversation({
|
||||
acceptedMessageRequest: true,
|
||||
systemGivenName: 'Alice',
|
||||
|
@ -110,3 +116,13 @@ export function SystemContact(args: PropsType): JSX.Element {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function WithSharedGroups(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...args}
|
||||
conversation={conversationWithSharedGroups}
|
||||
isSignalConnection
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue