Modernize ContactModal
This commit is contained in:
parent
1d2fcde49f
commit
c05d23e628
21 changed files with 426 additions and 493 deletions
|
@ -1,17 +1,29 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { ContactModalStateType } from '../state/ducks/globalModals';
|
||||
|
||||
type PropsType = {
|
||||
// ContactModal
|
||||
contactModalState?: ContactModalStateType;
|
||||
renderContactModal: () => JSX.Element;
|
||||
// ProfileEditor
|
||||
isProfileEditorVisible: boolean;
|
||||
renderProfileEditor: () => JSX.Element;
|
||||
};
|
||||
|
||||
export const GlobalModalContainer = ({
|
||||
// ContactModal
|
||||
contactModalState,
|
||||
renderContactModal,
|
||||
// ProfileEditor
|
||||
isProfileEditorVisible,
|
||||
renderProfileEditor,
|
||||
}: PropsType): JSX.Element | null => {
|
||||
if (contactModalState) {
|
||||
return renderContactModal();
|
||||
}
|
||||
|
||||
if (isProfileEditorVisible) {
|
||||
return renderProfileEditor();
|
||||
}
|
||||
|
|
|
@ -28,15 +28,17 @@ const defaultContact: ConversationType = getDefaultConversation({
|
|||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
areWeAdmin: boolean('areWeAdmin', overrideProps.areWeAdmin || false),
|
||||
contact: overrideProps.contact || defaultContact,
|
||||
hideContactModal: action('hideContactModal'),
|
||||
i18n,
|
||||
isAdmin: boolean('isAdmin', overrideProps.isAdmin || false),
|
||||
isMember: boolean('isMember', overrideProps.isMember || true),
|
||||
onClose: action('onClose'),
|
||||
openConversation: action('openConversation'),
|
||||
removeMember: action('removeMember'),
|
||||
showSafetyNumber: action('showSafetyNumber'),
|
||||
openConversationInternal: action('openConversationInternal'),
|
||||
removeMemberFromGroup: action('removeMemberFromGroup'),
|
||||
showSafetyNumberInConversation: action('showSafetyNumberInConversation'),
|
||||
toggleAdmin: action('toggleAdmin'),
|
||||
updateSharedGroups: action('updateSharedGroups'),
|
||||
updateConversationModelSharedGroups: action(
|
||||
'updateConversationModelSharedGroups'
|
||||
),
|
||||
});
|
||||
|
||||
story.add('As non-admin', () => {
|
||||
|
|
|
@ -1,103 +1,73 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { ReactPortal, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { About } from './About';
|
||||
import { Avatar } from '../Avatar';
|
||||
import { AvatarLightbox } from '../AvatarLightbox';
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
import { Modal } from '../Modal';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { SharedGroupNames } from '../SharedGroupNames';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
|
||||
export type PropsType = {
|
||||
export type PropsDataType = {
|
||||
areWeAdmin: boolean;
|
||||
contact?: ConversationType;
|
||||
conversationId?: string;
|
||||
readonly i18n: LocalizerType;
|
||||
isAdmin: boolean;
|
||||
isMember: boolean;
|
||||
onClose: () => void;
|
||||
openConversation: (conversationId: string) => void;
|
||||
removeMember: (conversationId: string) => void;
|
||||
showSafetyNumber: (conversationId: string) => void;
|
||||
toggleAdmin: (conversationId: string) => void;
|
||||
updateSharedGroups: () => void;
|
||||
};
|
||||
|
||||
type PropsActionType = {
|
||||
hideContactModal: () => void;
|
||||
openConversationInternal: (
|
||||
options: Readonly<{
|
||||
conversationId: string;
|
||||
messageId?: string;
|
||||
switchToAssociatedView?: boolean;
|
||||
}>
|
||||
) => void;
|
||||
removeMemberFromGroup: (conversationId: string, contactId: string) => void;
|
||||
showSafetyNumberInConversation: (conversationId: string) => void;
|
||||
toggleAdmin: (conversationId: string, contactId: string) => void;
|
||||
updateConversationModelSharedGroups: (conversationId: string) => void;
|
||||
};
|
||||
|
||||
export type PropsType = PropsDataType & PropsActionType;
|
||||
|
||||
export const ContactModal = ({
|
||||
areWeAdmin,
|
||||
contact,
|
||||
conversationId,
|
||||
hideContactModal,
|
||||
i18n,
|
||||
isAdmin,
|
||||
isMember,
|
||||
onClose,
|
||||
openConversation,
|
||||
removeMember,
|
||||
showSafetyNumber,
|
||||
openConversationInternal,
|
||||
removeMemberFromGroup,
|
||||
showSafetyNumberInConversation,
|
||||
toggleAdmin,
|
||||
updateSharedGroups,
|
||||
}: PropsType): ReactPortal | null => {
|
||||
updateConversationModelSharedGroups,
|
||||
}: PropsType): JSX.Element => {
|
||||
if (!contact) {
|
||||
throw new Error('Contact modal opened without a matching contact');
|
||||
}
|
||||
|
||||
const [root, setRoot] = useState<HTMLElement | null>(null);
|
||||
const overlayRef = useRef<HTMLElement | null>(null);
|
||||
const closeButtonRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
const [showingAvatar, setShowingAvatar] = useState(false);
|
||||
const [confirmToggleAdmin, setConfirmToggleAdmin] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
setRoot(div);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(div);
|
||||
setRoot(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Kick off the expensive hydration of the current sharedGroupNames
|
||||
updateSharedGroups();
|
||||
}, [updateSharedGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
if (root !== null && closeButtonRef.current) {
|
||||
closeButtonRef.current.focus();
|
||||
if (conversationId) {
|
||||
// Kick off the expensive hydration of the current sharedGroupNames
|
||||
updateConversationModelSharedGroups(conversationId);
|
||||
}
|
||||
}, [root]);
|
||||
}, [conversationId, updateConversationModelSharedGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keyup', handler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keyup', handler);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
const onClickOverlay = (e: React.MouseEvent<HTMLElement>) => {
|
||||
if (e.target === overlayRef.current) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
let content: JSX.Element;
|
||||
if (showingAvatar) {
|
||||
content = (
|
||||
return (
|
||||
<AvatarLightbox
|
||||
avatarColor={contact.color}
|
||||
avatarPath={contact.avatarPath}
|
||||
|
@ -106,18 +76,16 @@ export const ContactModal = ({
|
|||
onClose={() => setShowingAvatar(false)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<div className="module-contact-modal">
|
||||
<button
|
||||
ref={r => {
|
||||
closeButtonRef.current = r;
|
||||
}}
|
||||
type="button"
|
||||
className="module-contact-modal__close-button"
|
||||
onClick={onClose}
|
||||
aria-label={i18n('close')}
|
||||
/>
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
moduleClassName="ContactModal__modal"
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
onClose={hideContactModal}
|
||||
>
|
||||
<div className="ContactModal">
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarPath={contact.avatarPath}
|
||||
|
@ -133,53 +101,59 @@ export const ContactModal = ({
|
|||
unblurredAvatarPath={contact.unblurredAvatarPath}
|
||||
onClick={() => setShowingAvatar(true)}
|
||||
/>
|
||||
<div className="module-contact-modal__name">{contact.title}</div>
|
||||
<div className="ContactModal__name">{contact.title}</div>
|
||||
<div className="module-about__container">
|
||||
<About text={contact.about} />
|
||||
</div>
|
||||
{contact.phoneNumber && (
|
||||
<div className="module-contact-modal__info">
|
||||
{contact.phoneNumber}
|
||||
<div className="ContactModal__info">{contact.phoneNumber}</div>
|
||||
)}
|
||||
{!contact.isMe && (
|
||||
<div className="ContactModal__info">
|
||||
<SharedGroupNames
|
||||
i18n={i18n}
|
||||
sharedGroupNames={contact.sharedGroupNames || []}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="module-contact-modal__info">
|
||||
<SharedGroupNames
|
||||
i18n={i18n}
|
||||
sharedGroupNames={contact.sharedGroupNames || []}
|
||||
/>
|
||||
</div>
|
||||
<div className="module-contact-modal__button-container">
|
||||
<div className="ContactModal__button-container">
|
||||
<button
|
||||
type="button"
|
||||
className="module-contact-modal__button module-contact-modal__send-message"
|
||||
onClick={() => openConversation(contact.id)}
|
||||
className="ContactModal__button ContactModal__send-message"
|
||||
onClick={() => {
|
||||
hideContactModal();
|
||||
openConversationInternal({ conversationId: contact.id });
|
||||
}}
|
||||
>
|
||||
<div className="module-contact-modal__bubble-icon">
|
||||
<div className="module-contact-modal__send-message__bubble-icon" />
|
||||
<div className="ContactModal__bubble-icon">
|
||||
<div className="ContactModal__send-message__bubble-icon" />
|
||||
</div>
|
||||
<span>{i18n('ContactModal--message')}</span>
|
||||
</button>
|
||||
{!contact.isMe && (
|
||||
<button
|
||||
type="button"
|
||||
className="module-contact-modal__button module-contact-modal__safety-number"
|
||||
onClick={() => showSafetyNumber(contact.id)}
|
||||
className="ContactModal__button ContactModal__safety-number"
|
||||
onClick={() => {
|
||||
hideContactModal();
|
||||
showSafetyNumberInConversation(contact.id);
|
||||
}}
|
||||
>
|
||||
<div className="module-contact-modal__bubble-icon">
|
||||
<div className="module-contact-modal__safety-number__bubble-icon" />
|
||||
<div className="ContactModal__bubble-icon">
|
||||
<div className="ContactModal__safety-number__bubble-icon" />
|
||||
</div>
|
||||
<span>{i18n('showSafetyNumber')}</span>
|
||||
</button>
|
||||
)}
|
||||
{!contact.isMe && areWeAdmin && isMember && (
|
||||
{!contact.isMe && areWeAdmin && isMember && conversationId && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="module-contact-modal__button module-contact-modal__make-admin"
|
||||
onClick={() => toggleAdmin(contact.id)}
|
||||
className="ContactModal__button ContactModal__make-admin"
|
||||
onClick={() => setConfirmToggleAdmin(true)}
|
||||
>
|
||||
<div className="module-contact-modal__bubble-icon">
|
||||
<div className="module-contact-modal__make-admin__bubble-icon" />
|
||||
<div className="ContactModal__bubble-icon">
|
||||
<div className="ContactModal__make-admin__bubble-icon" />
|
||||
</div>
|
||||
{isAdmin ? (
|
||||
<span>{i18n('ContactModal--rm-admin')}</span>
|
||||
|
@ -189,34 +163,38 @@ export const ContactModal = ({
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="module-contact-modal__button module-contact-modal__remove-from-group"
|
||||
onClick={() => removeMember(contact.id)}
|
||||
className="ContactModal__button ContactModal__remove-from-group"
|
||||
onClick={() =>
|
||||
removeMemberFromGroup(conversationId, contact.id)
|
||||
}
|
||||
>
|
||||
<div className="module-contact-modal__bubble-icon">
|
||||
<div className="module-contact-modal__remove-from-group__bubble-icon" />
|
||||
<div className="ContactModal__bubble-icon">
|
||||
<div className="ContactModal__remove-from-group__bubble-icon" />
|
||||
</div>
|
||||
<span>{i18n('ContactModal--remove-from-group')}</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{confirmToggleAdmin && conversationId && (
|
||||
<ConfirmationDialog
|
||||
actions={[
|
||||
{
|
||||
action: () => toggleAdmin(conversationId, contact.id),
|
||||
text: isAdmin
|
||||
? i18n('ContactModal--rm-admin')
|
||||
: i18n('ContactModal--make-admin'),
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={() => setConfirmToggleAdmin(false)}
|
||||
>
|
||||
{isAdmin
|
||||
? i18n('ContactModal--rm-admin-info', [contact.title])
|
||||
: i18n('ContactModal--make-admin-info', [contact.title])}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return root
|
||||
? createPortal(
|
||||
<div
|
||||
ref={ref => {
|
||||
overlayRef.current = ref;
|
||||
}}
|
||||
role="presentation"
|
||||
className="module-contact-modal__overlay"
|
||||
onClick={onClickOverlay}
|
||||
>
|
||||
{content}
|
||||
</div>,
|
||||
root
|
||||
)
|
||||
: null;
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -60,7 +60,6 @@ export type StateProps = {
|
|||
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
||||
setDisappearingMessages: (seconds: number) => void;
|
||||
showAllMedia: () => void;
|
||||
showContactModal: (conversationId: string) => void;
|
||||
showGroupChatColorEditor: () => void;
|
||||
showGroupLinkManagement: () => void;
|
||||
showGroupV2Permissions: () => void;
|
||||
|
@ -86,6 +85,7 @@ type ActionProps = {
|
|||
deleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
|
||||
replaceAvatar: ReplaceAvatarActionType;
|
||||
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||
showContactModal: (contactId: string, conversationId: string) => void;
|
||||
};
|
||||
|
||||
export type Props = StateProps & ActionProps;
|
||||
|
@ -329,6 +329,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
|
||||
<ConversationDetailsMembershipList
|
||||
canAddNewMembers={canEditGroupInfo}
|
||||
conversationId={conversation.id}
|
||||
i18n={i18n}
|
||||
memberships={memberships}
|
||||
showContactModal={showContactModal}
|
||||
|
|
|
@ -44,6 +44,7 @@ const createProps = (overrideProps: Partial<Props>): Props => ({
|
|||
canAddNewMembers: isBoolean(overrideProps.canAddNewMembers)
|
||||
? overrideProps.canAddNewMembers
|
||||
: false,
|
||||
conversationId: '123',
|
||||
i18n,
|
||||
memberships: overrideProps.memberships || [],
|
||||
showContactModal: action('showContactModal'),
|
||||
|
|
|
@ -19,10 +19,11 @@ export type GroupV2Membership = {
|
|||
|
||||
export type Props = {
|
||||
canAddNewMembers: boolean;
|
||||
conversationId: string;
|
||||
i18n: LocalizerType;
|
||||
maxShownMemberCount?: number;
|
||||
memberships: Array<GroupV2Membership>;
|
||||
showContactModal: (conversationId: string) => void;
|
||||
showContactModal: (contactId: string, conversationId: string) => void;
|
||||
startAddingNewMembers?: () => void;
|
||||
};
|
||||
|
||||
|
@ -67,6 +68,7 @@ function sortMemberships(
|
|||
|
||||
export const ConversationDetailsMembershipList: React.ComponentType<Props> = ({
|
||||
canAddNewMembers,
|
||||
conversationId,
|
||||
i18n,
|
||||
maxShownMemberCount = 5,
|
||||
memberships,
|
||||
|
@ -101,7 +103,7 @@ export const ConversationDetailsMembershipList: React.ComponentType<Props> = ({
|
|||
{sortedMemberships.slice(0, membersToShow).map(({ isAdmin, member }) => (
|
||||
<PanelRow
|
||||
key={member.id}
|
||||
onClick={() => showContactModal(member.id)}
|
||||
onClick={() => showContactModal(member.id, conversationId)}
|
||||
icon={
|
||||
<Avatar
|
||||
conversationType="direct"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue