Context menu for left pane list items
This commit is contained in:
parent
02dedc7157
commit
f61d8f38b0
43 changed files with 1046 additions and 110 deletions
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { FunctionComponent } from 'react';
|
||||
import React from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { HEADER_CONTACT_NAME_CLASS_NAME } from './BaseConversationListItem';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
|
@ -12,7 +12,11 @@ import { ContactName } from '../conversation/ContactName';
|
|||
import { About } from '../conversation/About';
|
||||
import { ListTile } from '../ListTile';
|
||||
import { Avatar, AvatarSize } from '../Avatar';
|
||||
import { ContextMenu } from '../ContextMenu';
|
||||
import { Intl } from '../Intl';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||
|
||||
export type ContactListItemConversationType = Pick<
|
||||
ConversationType,
|
||||
|
@ -23,10 +27,13 @@ export type ContactListItemConversationType = Pick<
|
|||
| 'color'
|
||||
| 'groupId'
|
||||
| 'id'
|
||||
| 'name'
|
||||
| 'isMe'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'systemGivenName'
|
||||
| 'systemFamilyName'
|
||||
| 'title'
|
||||
| 'type'
|
||||
| 'unblurredAvatarPath'
|
||||
|
@ -42,6 +49,11 @@ type PropsDataType = ContactListItemConversationType & {
|
|||
type PropsHousekeepingType = {
|
||||
i18n: LocalizerType;
|
||||
onClick?: (id: string) => void;
|
||||
onAudioCall?: (id: string) => void;
|
||||
onVideoCall?: (id: string) => void;
|
||||
onRemove?: (id: string) => void;
|
||||
onBlock?: (id: string) => void;
|
||||
hasContextMenu: boolean;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
||||
|
@ -54,19 +66,81 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
|||
avatarPath,
|
||||
badge,
|
||||
color,
|
||||
hasContextMenu,
|
||||
i18n,
|
||||
id,
|
||||
isMe,
|
||||
name,
|
||||
onClick,
|
||||
onAudioCall,
|
||||
onVideoCall,
|
||||
onRemove,
|
||||
onBlock,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
systemGivenName,
|
||||
systemFamilyName,
|
||||
theme,
|
||||
title,
|
||||
type,
|
||||
unblurredAvatarPath,
|
||||
uuid,
|
||||
}) {
|
||||
const [isConfirmingBlocking, setConfirmingBlocking] = useState(false);
|
||||
const [isConfirmingRemoving, setConfirmingRemoving] = useState(false);
|
||||
|
||||
const menuOptions = useMemo(
|
||||
() => [
|
||||
...(onClick
|
||||
? [
|
||||
{
|
||||
icon: 'ContactListItem__context-menu__chat-icon',
|
||||
label: i18n('icu:ContactListItem__menu__message'),
|
||||
onClick: () => onClick(id),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(onAudioCall
|
||||
? [
|
||||
{
|
||||
icon: 'ContactListItem__context-menu__phone-icon',
|
||||
label: i18n('icu:ContactListItem__menu__audio-call'),
|
||||
onClick: () => onAudioCall(id),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(onVideoCall
|
||||
? [
|
||||
{
|
||||
icon: 'ContactListItem__context-menu__video-icon',
|
||||
label: i18n('icu:ContactListItem__menu__video-call'),
|
||||
onClick: () => onVideoCall(id),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(onRemove
|
||||
? [
|
||||
{
|
||||
icon: 'ContactListItem__context-menu__delete-icon',
|
||||
label: i18n('icu:ContactListItem__menu__remove'),
|
||||
onClick: () => setConfirmingRemoving(true),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(onBlock
|
||||
? [
|
||||
{
|
||||
icon: 'ContactListItem__context-menu__block-icon',
|
||||
label: i18n('icu:ContactListItem__menu__block'),
|
||||
onClick: () => setConfirmingBlocking(true),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
[id, i18n, onClick, onAudioCall, onVideoCall, onRemove, onBlock]
|
||||
);
|
||||
|
||||
const headerName = isMe ? (
|
||||
<ContactName
|
||||
isMe={isMe}
|
||||
|
@ -84,32 +158,138 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
|||
const messageText =
|
||||
about && !isMe ? <About className="" text={about} /> : undefined;
|
||||
|
||||
return (
|
||||
<ListTile
|
||||
leading={
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPath={avatarPath}
|
||||
color={color}
|
||||
conversationType={type}
|
||||
noteToSelf={Boolean(isMe)}
|
||||
let trailing: JSX.Element | undefined;
|
||||
if (hasContextMenu) {
|
||||
trailing = (
|
||||
<ContextMenu
|
||||
i18n={i18n}
|
||||
menuOptions={menuOptions}
|
||||
popperOptions={{ placement: 'bottom-start', strategy: 'absolute' }}
|
||||
moduleClassName="ContactListItem__context-menu"
|
||||
ariaLabel={i18n('icu:ContactListItem__menu')}
|
||||
portalToRoot
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let blockConfirmation: JSX.Element | undefined;
|
||||
let removeConfirmation: JSX.Element | undefined;
|
||||
|
||||
if (isConfirmingBlocking) {
|
||||
blockConfirmation = (
|
||||
<ConfirmationDialog
|
||||
dialogName="ContactListItem.blocking"
|
||||
i18n={i18n}
|
||||
onClose={() => setConfirmingBlocking(false)}
|
||||
title={
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:MessageRequests--block-direct-confirm-title"
|
||||
components={{
|
||||
title: <ContactName key="name" title={title} />,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
text: i18n('icu:MessageRequests--block'),
|
||||
action: () => onBlock?.(id),
|
||||
style: 'negative',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{i18n('icu:MessageRequests--block-direct-confirm-body')}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
if (isConfirmingRemoving) {
|
||||
if (
|
||||
isInSystemContacts({ type, name, systemGivenName, systemFamilyName })
|
||||
) {
|
||||
removeConfirmation = (
|
||||
<ConfirmationDialog
|
||||
key="ContactListItem.systemContact"
|
||||
dialogName="ContactListItem.systemContact"
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
unblurredAvatarPath={unblurredAvatarPath}
|
||||
// This is here to appease the type checker.
|
||||
{...(badge ? { badge, theme } : { badge: undefined })}
|
||||
/>
|
||||
}
|
||||
title={headerName}
|
||||
subtitle={messageText}
|
||||
subtitleMaxLines={1}
|
||||
onClick={onClick ? () => onClick(id) : undefined}
|
||||
/>
|
||||
onClose={() => setConfirmingRemoving(false)}
|
||||
title={
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:ContactListItem__remove-system--title"
|
||||
components={{
|
||||
title: <ContactName key="name" title={title} />,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
cancelText={i18n('icu:Confirmation--confirm')}
|
||||
>
|
||||
{i18n('icu:ContactListItem__remove-system--body')}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
} else {
|
||||
removeConfirmation = (
|
||||
<ConfirmationDialog
|
||||
key="ContactListItem.removing"
|
||||
dialogName="ContactListItem.removing"
|
||||
i18n={i18n}
|
||||
onClose={() => setConfirmingRemoving(false)}
|
||||
title={
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:ContactListItem__remove--title"
|
||||
components={{
|
||||
title: <ContactName key="name" title={title} />,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
text: i18n('icu:ContactListItem__remove--confirm'),
|
||||
action: () => onRemove?.(id),
|
||||
style: 'negative',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{i18n('icu:ContactListItem__remove--body')}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListTile
|
||||
moduleClassName="ContactListItem"
|
||||
leading={
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPath={avatarPath}
|
||||
color={color}
|
||||
conversationType={type}
|
||||
noteToSelf={Boolean(isMe)}
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
unblurredAvatarPath={unblurredAvatarPath}
|
||||
// This is here to appease the type checker.
|
||||
{...(badge ? { badge, theme } : { badge: undefined })}
|
||||
/>
|
||||
}
|
||||
trailing={trailing}
|
||||
title={headerName}
|
||||
subtitle={messageText}
|
||||
subtitleMaxLines={1}
|
||||
onClick={onClick ? () => onClick(id) : undefined}
|
||||
/>
|
||||
|
||||
{blockConfirmation}
|
||||
{removeConfirmation}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -53,6 +53,7 @@ export type PropsData = Pick<
|
|||
| 'muteExpiresAt'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'removalStage'
|
||||
| 'sharedGroupNames'
|
||||
| 'shouldShowDraft'
|
||||
| 'title'
|
||||
|
@ -92,6 +93,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
|||
onClick,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
removalStage,
|
||||
sharedGroupNames,
|
||||
shouldShowDraft,
|
||||
theme,
|
||||
|
@ -125,7 +127,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
|||
let messageText: ReactNode = null;
|
||||
let messageStatusIcon: ReactNode = null;
|
||||
|
||||
if (!acceptedMessageRequest) {
|
||||
if (!acceptedMessageRequest && removalStage !== 'justNotification') {
|
||||
messageText = (
|
||||
<span className={`${MESSAGE_TEXT_CLASS_NAME}__message-request`}>
|
||||
{i18n('icu:ConversationListItem--message-request')}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue