From 0dd9b9ec984b9ce1a0c0a25ea6967e81f8789acb Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:09:33 -0500 Subject: [PATCH] View contact modal from call participants list Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com> --- _locales/en/messages.json | 8 + stylesheets/_modules.scss | 10 + stylesheets/components/ContactModal.scss | 4 + ts/components/CallManager.stories.tsx | 1 + ts/components/CallManager.tsx | 10 + .../CallingAdhocCallInfo.stories.tsx | 1 + ts/components/CallingAdhocCallInfo.tsx | 34 +++- .../CallingParticipantsList.stories.tsx | 2 + ts/components/CallingParticipantsList.tsx | 26 ++- ts/components/conversation/ContactModal.tsx | 175 ++++++++++++------ ts/state/smart/CallManager.tsx | 4 +- ts/state/smart/ContactModal.tsx | 9 +- ts/util/theme.ts | 11 ++ 13 files changed, 233 insertions(+), 62 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 4407054f5..8a3361508 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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" diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 2051fac27..88a075265 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -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; diff --git a/stylesheets/components/ContactModal.scss b/stylesheets/components/ContactModal.scss index d6a1fb006..cc3ac3770 100644 --- a/stylesheets/components/ContactModal.scss +++ b/stylesheets/components/ContactModal.scss @@ -309,4 +309,8 @@ margin-block: 8px 5px; } + + &__tooltip { + @include tooltip; + } } diff --git a/ts/components/CallManager.stories.tsx b/ts/components/CallManager.stories.tsx index b8c782b4e..4414bbe48 100644 --- a/ts/components/CallManager.stories.tsx +++ b/ts/components/CallManager.stories.tsx @@ -113,6 +113,7 @@ const createProps = (storyProps: Partial = {}): 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'), diff --git a/ts/components/CallManager.tsx b/ts/components/CallManager.tsx index 364802946..f2f3fc17d 100644 --- a/ts/components/CallManager.tsx +++ b/ts/components/CallManager.tsx @@ -96,6 +96,7 @@ export type PropsType = { renderReactionPicker: ( props: React.ComponentProps ) => 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} /> ) : ( ))} @@ -489,13 +494,16 @@ function ActiveCallManager({ onCopyCallLink={onCopyCallLink} onShareCallLinkViaSignal={handleShareCallLinkViaSignal} removeClient={removeClient} + 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} diff --git a/ts/components/CallingAdhocCallInfo.stories.tsx b/ts/components/CallingAdhocCallInfo.stories.tsx index 6e7fd24e6..9efd3fc12 100644 --- a/ts/components/CallingAdhocCallInfo.stories.tsx +++ b/ts/components/CallingAdhocCallInfo.stories.tsx @@ -68,6 +68,7 @@ const createProps = (overrideProps: Partial = {}): 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 { diff --git a/ts/components/CallingAdhocCallInfo.tsx b/ts/components/CallingAdhocCallInfo.tsx index 8ffe7b5d4..0b36e036d 100644 --- a/ts/components/CallingAdhocCallInfo.tsx +++ b/ts/components/CallingAdhocCallInfo.tsx @@ -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) => ( -
  • { + if (participant.isMe) { + return; + } + + onClose(); + showContactModal(participant.id); + }} + type="button" >
    { + onClick={event => { if (!participant.demuxId) { return; } + + event.stopPropagation(); + event.preventDefault(); removeClient({ demuxId: participant.demuxId }); }} type="button" /> ) : null} -
  • + ), - [i18n, isCallLinkAdmin, ourServiceId, removeClient] + [ + i18n, + isCallLinkAdmin, + onClose, + ourServiceId, + removeClient, + showContactModal, + ] ); return ( diff --git a/ts/components/CallingParticipantsList.stories.tsx b/ts/components/CallingParticipantsList.stories.tsx index b325903e1..fd5851c41 100644 --- a/ts/components/CallingParticipantsList.stories.tsx +++ b/ts/components/CallingParticipantsList.stories.tsx @@ -43,9 +43,11 @@ function createParticipant( const createProps = (overrideProps: Partial = {}): PropsType => ({ i18n, + conversationId: 'fake-conversation-id', onClose: action('on-close'), ourServiceId: generateAci(), participants: overrideProps.participants || [], + showContactModal: action('show-contact-modal'), }); export default { diff --git a/ts/components/CallingParticipantsList.tsx b/ts/components/CallingParticipantsList.tsx index f2b570488..9a3e4755d 100644 --- a/ts/components/CallingParticipantsList.tsx +++ b/ts/components/CallingParticipantsList.tsx @@ -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; + 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(null); @@ -96,15 +103,26 @@ export const CallingParticipantsList = React.memo( aria-label={i18n('icu:close')} /> -
      +
      {sortedParticipants.map( (participant: ParticipantType, index: number) => ( -
    • { + if (participant.isMe) { + return; + } + + onClose(); + showContactModal(participant.id, conversationId); + }} + type="button" >
      -
    • + ) )} -
    + , diff --git a/ts/components/conversation/ContactModal.tsx b/ts/components/conversation/ContactModal.tsx index e3b6147e4..adb4bf79c 100644 --- a/ts/components/conversation/ContactModal.tsx +++ b/ts/components/conversation/ContactModal.tsx @@ -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.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 = ( + + ); + const audioCallButton = ( + + ); + + return ( +
    + + {hasActiveCall ? ( + + {videoCallButton} + + ) : ( + videoCallButton + )} + {hasActiveCall ? ( + + {audioCallButton} + + ) : ( + audioCallButton + )} +
    + ); + }, + [ + 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} >
    - {!contact.isMe && ( -
    - - - -
    - )} + {!contact.isMe && renderQuickActions(contact.id)}
    {canHaveNicknameAndNote(contact) && ( @@ -319,20 +378,32 @@ export function ContactModal({ )} - {!contact.isMe && ( - - )} + ) : ( + + ))} {!contact.isMe && (