Context menu for left pane list items

This commit is contained in:
Fedor Indutny 2023-04-05 13:48:00 -07:00 committed by GitHub
parent 02dedc7157
commit f61d8f38b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 1046 additions and 110 deletions

View file

@ -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}
</>
);
}
);

View file

@ -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')}