View contact modal from call participants list

This commit is contained in:
ayumi-signal 2024-06-18 09:15:56 -07:00 committed by GitHub
parent 49a6fa6007
commit 378bd7487f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 233 additions and 62 deletions

View file

@ -3686,6 +3686,10 @@
"messageformat": "A window",
"description": "Title for the select your screen sharing sources modal"
},
"icu:calling__ParticipantInfoButton": {
"messageformat": "More info about this contact",
"description": "Aria label for clickable contact info button in the in-call participant info popup. When clicked a popup appears with the contact's details and options to message them."
},
"icu:CallingAdhocCallInfo__CopyLink": {
"messageformat": "Copy call link",
"description": "Menu item in the in-call info popup for call link calls. The action is to add the call link to the clipboard."
@ -4666,6 +4670,10 @@
"messageformat": "Remove from group",
"description": "Button text for remove from group button in Group Contact Details modal"
},
"icu:ContactModal--already-in-call": {
"messageformat": "You are already in a call",
"description": "Tooltip text for video or audio call button in Contact Details modal"
},
"icu:showChatColorEditor": {
"messageformat": "Chat color",
"description": "This is a button in the conversation context menu to show the chat color editor"

View file

@ -4450,17 +4450,23 @@ button.module-image__border-overlay:focus {
&__contact {
@include font-body-1;
@include button-reset;
display: flex;
align-items: center;
width: 100%;
margin-block: 2px;
padding-block: 8px;
padding-inline-start: 10px;
padding-inline-end: 2px;
list-style-type: none;
border-radius: 6px;
cursor: auto;
&:hover {
background-color: $color-gray-62;
}
&[disabled] {
cursor: auto;
}
}
&__avatar-and-name {
@ -4551,6 +4557,10 @@ button.module-image__border-overlay:focus {
}
}
button.module-calling-participants-list__contact {
cursor: pointer;
}
.module-call-need-permission-screen {
align-items: center;
background-color: $color-gray-95;

View file

@ -309,4 +309,8 @@
margin-block: 8px 5px;
}
&__tooltip {
@include tooltip;
}
}

View file

@ -113,6 +113,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
setPresenting: action('toggle-presenting'),
setRendererCanvas: action('set-renderer-canvas'),
setOutgoingRing: action('set-outgoing-ring'),
showContactModal: action('show-contact-modal'),
showShareCallLinkViaSignal: action('show-share-call-link-via-signal'),
startCall: action('start-call'),
stopRingtone: action('stop-ringtone'),

View file

@ -96,6 +96,7 @@ export type PropsType = {
renderReactionPicker: (
props: React.ComponentProps<typeof SmartReactionPicker>
) => JSX.Element;
showContactModal: (contactId: string, conversationId?: string) => void;
startCall: (payload: StartCallType) => void;
toggleParticipants: () => void;
acceptCall: (_: AcceptCallType) => void;
@ -188,6 +189,7 @@ function ActiveCallManager({
setPresenting,
setRendererCanvas,
setOutgoingRing,
showContactModal,
showShareCallLinkViaSignal,
startCall,
switchToPresentationView,
@ -395,13 +397,16 @@ function ActiveCallManager({
onCopyCallLink={onCopyCallLink}
onShareCallLinkViaSignal={handleShareCallLinkViaSignal}
removeClient={removeClient}
showContactModal={showContactModal}
/>
) : (
<CallingParticipantsList
conversationId={conversation.id}
i18n={i18n}
onClose={toggleParticipants}
ourServiceId={me.serviceId}
participants={peekedParticipants}
showContactModal={showContactModal}
/>
))}
</>
@ -489,13 +494,16 @@ function ActiveCallManager({
onCopyCallLink={onCopyCallLink}
onShareCallLinkViaSignal={handleShareCallLinkViaSignal}
removeClient={removeClient}
showContactModal={showContactModal}
/>
) : (
<CallingParticipantsList
conversationId={conversation.id}
i18n={i18n}
onClose={toggleParticipants}
ourServiceId={me.serviceId}
participants={groupCallParticipantsForParticipantsList}
showContactModal={showContactModal}
/>
))}
</>
@ -543,6 +551,7 @@ export function CallManager({
setOutgoingRing,
setPresenting,
setRendererCanvas,
showContactModal,
showShareCallLinkViaSignal,
startCall,
stopRingtone,
@ -633,6 +642,7 @@ export function CallManager({
setOutgoingRing={setOutgoingRing}
setPresenting={setPresenting}
setRendererCanvas={setRendererCanvas}
showContactModal={showContactModal}
showShareCallLinkViaSignal={showShareCallLinkViaSignal}
startCall={startCall}
switchFromPresentationView={switchFromPresentationView}

View file

@ -68,6 +68,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
onCopyCallLink: action('on-copy-call-link'),
onShareCallLinkViaSignal: action('on-share-call-link-via-signal'),
removeClient: overrideProps.removeClient || action('remove-client'),
showContactModal: action('show-contact-modal'),
});
export default {

View file

@ -41,6 +41,10 @@ export type PropsType = {
readonly onCopyCallLink: () => void;
readonly onShareCallLinkViaSignal: () => void;
readonly removeClient: ((payload: RemoveClientType) => void) | null;
readonly showContactModal: (
contactId: string,
conversationId?: string
) => void;
};
type UnknownContactsPropsType = {
@ -145,6 +149,7 @@ export function CallingAdhocCallInfo({
onCopyCallLink,
onShareCallLinkViaSignal,
removeClient,
showContactModal,
}: PropsType): JSX.Element | null {
const [isUnknownContactDialogVisible, setIsUnknownContactDialogVisible] =
React.useState(false);
@ -173,12 +178,23 @@ export function CallingAdhocCallInfo({
const renderParticipant = React.useCallback(
(participant: ParticipantType, key: React.Key) => (
<li
<button
aria-label={i18n('icu:calling__ParticipantInfoButton')}
className="module-calling-participants-list__contact"
disabled={participant.isMe}
// 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}
onClick={() => {
if (participant.isMe) {
return;
}
onClose();
showContactModal(participant.id);
}}
type="button"
>
<div className="module-calling-participants-list__avatar-and-name">
<Avatar
@ -250,18 +266,28 @@ export function CallingAdhocCallInfo({
'module-calling-participants-list__status-icon',
'module-calling-participants-list__remove'
)}
onClick={() => {
onClick={event => {
if (!participant.demuxId) {
return;
}
event.stopPropagation();
event.preventDefault();
removeClient({ demuxId: participant.demuxId });
}}
type="button"
/>
) : null}
</li>
</button>
),
[i18n, isCallLinkAdmin, ourServiceId, removeClient]
[
i18n,
isCallLinkAdmin,
onClose,
ourServiceId,
removeClient,
showContactModal,
]
);
return (

View file

@ -43,9 +43,11 @@ function createParticipant(
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
i18n,
conversationId: 'fake-conversation-id',
onClose: action('on-close'),
ourServiceId: generateAci(),
participants: overrideProps.participants || [],
showContactModal: action('show-contact-modal'),
});
export default {

View file

@ -26,18 +26,25 @@ type ParticipantType = ConversationType & {
};
export type PropsType = {
readonly conversationId: string;
readonly i18n: LocalizerType;
readonly onClose: () => void;
readonly ourServiceId: ServiceIdString | undefined;
readonly participants: Array<ParticipantType>;
readonly showContactModal: (
contactId: string,
conversationId?: string
) => void;
};
export const CallingParticipantsList = React.memo(
function CallingParticipantsListInner({
conversationId,
i18n,
onClose,
ourServiceId,
participants,
showContactModal,
}: PropsType) {
const [root, setRoot] = React.useState<HTMLElement | null>(null);
@ -96,15 +103,26 @@ export const CallingParticipantsList = React.memo(
aria-label={i18n('icu:close')}
/>
</div>
<ul className="module-calling-participants-list__list">
<div className="module-calling-participants-list__list">
{sortedParticipants.map(
(participant: ParticipantType, index: number) => (
<li
<button
aria-label={i18n('icu:calling__ParticipantInfoButton')}
className="module-calling-participants-list__contact"
disabled={participant.isMe}
// 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}
onClick={() => {
if (participant.isMe) {
return;
}
onClose();
showContactModal(participant.id, conversationId);
}}
type="button"
>
<div className="module-calling-participants-list__avatar-and-name">
<Avatar
@ -165,10 +183,10 @@ export const CallingParticipantsList = React.memo(
'module-calling-participants-list__muted--audio'
)}
/>
</li>
</button>
)
)}
</ul>
</div>
</div>
</div>
</FocusTrap>,

View file

@ -26,6 +26,9 @@ import { Button, ButtonIconType, ButtonVariant } from '../Button';
import { isInSystemContacts } from '../../util/isInSystemContacts';
import { InContactsIcon } from '../InContactsIcon';
import { canHaveNicknameAndNote } from '../../util/nicknames';
import { Tooltip, TooltipPlacement } from '../Tooltip';
import { offsetDistanceModifier } from '../../util/popperUtil';
import { getThemeByThemeType } from '../../util/theme';
export type PropsDataType = {
areWeASubscriber: boolean;
@ -39,6 +42,7 @@ export type PropsDataType = {
isMember: boolean;
theme: ThemeType;
hasActiveCall: boolean;
isInFullScreenCall: boolean;
};
type PropsActionType = {
@ -51,6 +55,7 @@ type PropsActionType = {
showConversation: ShowConversationType;
toggleAdmin: (conversationId: string, contactId: string) => void;
toggleAboutContactModal: (conversationId: string) => unknown;
togglePip: () => void;
toggleSafetyNumberModal: (conversationId: string) => unknown;
toggleAddUserToAnotherGroupModal: (conversationId: string) => void;
updateConversationModelSharedGroups: (conversationId: string) => void;
@ -82,6 +87,7 @@ export function ContactModal({
hasActiveCall,
hasStories,
hideContactModal,
isInFullScreenCall,
i18n,
isAdmin,
isMember,
@ -94,6 +100,7 @@ export function ContactModal({
toggleAboutContactModal,
toggleAddUserToAnotherGroupModal,
toggleAdmin,
togglePip,
toggleSafetyNumberModal,
updateConversationModelSharedGroups,
viewUserStories,
@ -106,6 +113,7 @@ export function ContactModal({
const [subModalState, setSubModalState] = useState<SubModalState>(
SubModalState.None
);
const modalTheme = getThemeByThemeType(theme);
useEffect(() => {
if (contact?.id) {
@ -114,6 +122,94 @@ export function ContactModal({
}
}, [contact?.id, updateConversationModelSharedGroups]);
const renderQuickActions = React.useCallback(
(conversationId: string) => {
const videoCallButton = (
<Button
icon={ButtonIconType.video}
variant={ButtonVariant.Details}
disabled={hasActiveCall}
onClick={() => {
hideContactModal();
onOutgoingVideoCallInConversation(conversationId);
}}
>
{i18n('icu:video')}
</Button>
);
const audioCallButton = (
<Button
icon={ButtonIconType.audio}
variant={ButtonVariant.Details}
disabled={hasActiveCall}
onClick={() => {
hideContactModal();
onOutgoingAudioCallInConversation(conversationId);
}}
>
{i18n('icu:audio')}
</Button>
);
return (
<div className="ContactModal__quick-actions">
<Button
icon={ButtonIconType.message}
variant={ButtonVariant.Details}
onClick={() => {
hideContactModal();
showConversation({
conversationId,
switchToAssociatedView: true,
});
if (isInFullScreenCall) {
togglePip();
}
}}
>
{i18n('icu:ConversationDetails__HeaderButton--Message')}
</Button>
{hasActiveCall ? (
<Tooltip
className="ContactModal__tooltip"
wrapperClassName="ContactModal__tooltip-wrapper"
content={i18n('icu:ContactModal--already-in-call')}
direction={TooltipPlacement.Top}
popperModifiers={[offsetDistanceModifier(5)]}
>
{videoCallButton}
</Tooltip>
) : (
videoCallButton
)}
{hasActiveCall ? (
<Tooltip
className="ContactModal__tooltip"
wrapperClassName="ContactModal__tooltip-wrapper"
content={i18n('icu:ContactModal--already-in-call')}
direction={TooltipPlacement.Top}
popperModifiers={[offsetDistanceModifier(5)]}
>
{audioCallButton}
</Tooltip>
) : (
audioCallButton
)}
</div>
);
},
[
hasActiveCall,
hideContactModal,
i18n,
isInFullScreenCall,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
showConversation,
togglePip,
]
);
let modalNode: ReactNode;
switch (subModalState) {
case SubModalState.None:
@ -213,6 +309,7 @@ export function ContactModal({
i18n={i18n}
onClose={hideContactModal}
padded={false}
theme={modalTheme}
>
<div className="ContactModal">
<Avatar
@ -265,45 +362,7 @@ export function ContactModal({
</div>
<i className="ContactModal__name__chevron" />
</button>
{!contact.isMe && (
<div className="ContactModal__quick-actions">
<Button
icon={ButtonIconType.message}
variant={ButtonVariant.Details}
onClick={() => {
hideContactModal();
showConversation({
conversationId: contact?.id,
switchToAssociatedView: true,
});
}}
>
{i18n('icu:ConversationDetails__HeaderButton--Message')}
</Button>
<Button
icon={ButtonIconType.video}
variant={ButtonVariant.Details}
disabled={hasActiveCall}
onClick={() => {
hideContactModal();
onOutgoingVideoCallInConversation(contact.id);
}}
>
{i18n('icu:video')}
</Button>
<Button
icon={ButtonIconType.audio}
variant={ButtonVariant.Details}
disabled={hasActiveCall}
onClick={() => {
hideContactModal();
onOutgoingAudioCallInConversation(contact.id);
}}
>
{i18n('icu:audio')}
</Button>
</div>
)}
{!contact.isMe && renderQuickActions(contact.id)}
<div className="ContactModal__divider" />
<div className="ContactModal__button-container">
{canHaveNicknameAndNote(contact) && (
@ -319,20 +378,32 @@ export function ContactModal({
</button>
)}
{!contact.isMe && (
<button
type="button"
className="ContactModal__button ContactModal__block"
onClick={() =>
setSubModalState(SubModalState.ConfirmingBlock)
}
>
<div className="ContactModal__bubble-icon">
<div className="ContactModal__block__bubble-icon" />
{!contact.isMe &&
(contact.isBlocked ? (
<div className="ContactModal__button ContactModal__block">
<div className="ContactModal__bubble-icon">
<div className="ContactModal__block__bubble-icon" />
</div>
<span>
{i18n('icu:AboutContactModal__blocked', {
name: contact.title,
})}
</span>
</div>
<span>{i18n('icu:MessageRequests--block')}</span>
</button>
)}
) : (
<button
type="button"
className="ContactModal__button ContactModal__block"
onClick={() =>
setSubModalState(SubModalState.ConfirmingBlock)
}
>
<div className="ContactModal__bubble-icon">
<div className="ContactModal__block__bubble-icon" />
</div>
<span>{i18n('icu:MessageRequests--block')}</span>
</button>
))}
{!contact.isMe && (
<button
type="button"

View file

@ -455,7 +455,8 @@ export const SmartCallManager = memo(function SmartCallManager() {
toggleSettings,
} = useCallingActions();
const { pauseVoiceNotePlayer } = useAudioPlayerActions();
const { showShareCallLinkViaSignal } = useGlobalModalActions();
const { showContactModal, showShareCallLinkViaSignal } =
useGlobalModalActions();
return (
<CallManager
@ -501,6 +502,7 @@ export const SmartCallManager = memo(function SmartCallManager() {
setOutgoingRing={setOutgoingRing}
setPresenting={setPresenting}
setRendererCanvas={setRendererCanvas}
showContactModal={showContactModal}
showShareCallLinkViaSignal={showShareCallLinkViaSignal}
startCall={startCall}
stopRingtone={stopRingtone}

View file

@ -9,7 +9,10 @@ import { getIntl, getTheme } from '../selectors/user';
import { getBadgesSelector } from '../selectors/badges';
import { getConversationSelector } from '../selectors/conversations';
import { getHasStoriesSelector } from '../selectors/stories2';
import { getActiveCallState } from '../selectors/calling';
import {
getActiveCallState,
isInFullScreenCall as getIsInFullScreenCall,
} from '../selectors/calling';
import { useStoriesActions } from '../ducks/stories';
import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals';
@ -24,6 +27,7 @@ export const SmartContactModal = memo(function SmartContactModal() {
const conversationSelector = useSelector(getConversationSelector);
const hasStoriesSelector = useSelector(getHasStoriesSelector);
const activeCallState = useSelector(getActiveCallState);
const isInFullScreenCall = useSelector(getIsInFullScreenCall);
const badgesSelector = useSelector(getBadgesSelector);
const areWeASubscriber = useSelector(getAreWeASubscriber);
@ -62,6 +66,7 @@ export const SmartContactModal = memo(function SmartContactModal() {
const {
onOutgoingVideoCallInConversation,
onOutgoingAudioCallInConversation,
togglePip,
} = useCallingActions();
const handleOpenEditNicknameAndNoteModal = useCallback(() => {
@ -82,6 +87,7 @@ export const SmartContactModal = memo(function SmartContactModal() {
hideContactModal={hideContactModal}
i18n={i18n}
isAdmin={isAdmin}
isInFullScreenCall={isInFullScreenCall}
isMember={isMember}
onOpenEditNicknameAndNoteModal={handleOpenEditNicknameAndNoteModal}
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
@ -92,6 +98,7 @@ export const SmartContactModal = memo(function SmartContactModal() {
toggleAboutContactModal={toggleAboutContactModal}
toggleAddUserToAnotherGroupModal={toggleAddUserToAnotherGroupModal}
toggleAdmin={toggleAdmin}
togglePip={togglePip}
toggleSafetyNumberModal={toggleSafetyNumberModal}
updateConversationModelSharedGroups={updateConversationModelSharedGroups}
viewUserStories={viewUserStories}

View file

@ -30,3 +30,14 @@ export function themeClassName2(theme: ThemeType): string {
throw missingCaseError(theme);
}
}
export function getThemeByThemeType(theme: ThemeType): Theme {
switch (theme) {
case ThemeType.light:
return Theme.Light;
case ThemeType.dark:
return Theme.Dark;
default:
throw missingCaseError(theme);
}
}