Conversation details screen for 1:1 chats
This commit is contained in:
parent
3a507349cd
commit
2e438aa876
35 changed files with 1357 additions and 1102 deletions
|
@ -11,15 +11,7 @@ const story = storiesOf('Components/Button', module);
|
|||
|
||||
story.add('Kitchen sink', () => (
|
||||
<>
|
||||
{[
|
||||
ButtonVariant.Primary,
|
||||
ButtonVariant.Secondary,
|
||||
ButtonVariant.SecondaryAffirmative,
|
||||
ButtonVariant.SecondaryDestructive,
|
||||
ButtonVariant.Destructive,
|
||||
ButtonVariant.Calling,
|
||||
ButtonVariant.SystemMessage,
|
||||
].map(variant => (
|
||||
{Object.values(ButtonVariant).map(variant => (
|
||||
<React.Fragment key={variant}>
|
||||
{[ButtonSize.Medium, ButtonSize.Small].map(size => (
|
||||
<React.Fragment key={size}>
|
||||
|
|
|
@ -12,18 +12,30 @@ export enum ButtonSize {
|
|||
}
|
||||
|
||||
export enum ButtonVariant {
|
||||
Calling = 'Calling',
|
||||
Destructive = 'Destructive',
|
||||
Details = 'Details',
|
||||
Primary = 'Primary',
|
||||
Secondary = 'Secondary',
|
||||
SecondaryAffirmative = 'SecondaryAffirmative',
|
||||
SecondaryDestructive = 'SecondaryDestructive',
|
||||
Destructive = 'Destructive',
|
||||
Calling = 'Calling',
|
||||
SystemMessage = 'SystemMessage',
|
||||
}
|
||||
|
||||
export enum ButtonIconType {
|
||||
audio = 'audio',
|
||||
muted = 'muted',
|
||||
photo = 'photo',
|
||||
search = 'search',
|
||||
text = 'text',
|
||||
unmuted = 'unmuted',
|
||||
video = 'video',
|
||||
}
|
||||
|
||||
type PropsType = {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
icon?: ButtonIconType;
|
||||
size?: ButtonSize;
|
||||
style?: CSSProperties;
|
||||
tabIndex?: number;
|
||||
|
@ -70,6 +82,7 @@ const VARIANT_CLASS_NAMES = new Map<ButtonVariant, string>([
|
|||
[ButtonVariant.Destructive, 'module-Button--destructive'],
|
||||
[ButtonVariant.Calling, 'module-Button--calling'],
|
||||
[ButtonVariant.SystemMessage, 'module-Button--system-message'],
|
||||
[ButtonVariant.Details, 'module-Button--details'],
|
||||
]);
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
|
||||
|
@ -78,10 +91,13 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
|
|||
children,
|
||||
className,
|
||||
disabled = false,
|
||||
size = ButtonSize.Medium,
|
||||
icon,
|
||||
style,
|
||||
tabIndex,
|
||||
variant = ButtonVariant.Primary,
|
||||
size = variant === ButtonVariant.Details
|
||||
? ButtonSize.Small
|
||||
: ButtonSize.Medium,
|
||||
} = props;
|
||||
const ariaLabel = props['aria-label'];
|
||||
|
||||
|
@ -108,6 +124,7 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
|
|||
'module-Button',
|
||||
sizeClassName,
|
||||
variantClassName,
|
||||
`module-Button__icon--${icon}`,
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -10,6 +10,7 @@ export type PropsType = {
|
|||
checked?: boolean;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
isRadio?: boolean;
|
||||
label: string;
|
||||
moduleClassName?: string;
|
||||
name: string;
|
||||
|
@ -20,6 +21,7 @@ export const Checkbox = ({
|
|||
checked,
|
||||
description,
|
||||
disabled,
|
||||
isRadio,
|
||||
label,
|
||||
moduleClassName,
|
||||
name,
|
||||
|
@ -37,7 +39,7 @@ export const Checkbox = ({
|
|||
id={id}
|
||||
name={name}
|
||||
onChange={ev => onChange(ev.target.checked)}
|
||||
type="checkbox"
|
||||
type={isRadio ? 'radio' : 'checkbox'}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -440,7 +440,7 @@ export const Preferences = ({
|
|||
}}
|
||||
right={
|
||||
<div
|
||||
className={`module-conversation-details__chat-color module-conversation-details__chat-color--${defaultConversationColor.color}`}
|
||||
className={`ConversationDetails__chat-color ConversationDetails__chat-color--${defaultConversationColor.color}`}
|
||||
style={{
|
||||
...getCustomColorStyle(
|
||||
defaultConversationColor.customColorData?.value
|
||||
|
|
|
@ -39,7 +39,6 @@ const commonProps = {
|
|||
onShowConversationDetails: action('onShowConversationDetails'),
|
||||
onSetDisappearingMessages: action('onSetDisappearingMessages'),
|
||||
onDeleteMessages: action('onDeleteMessages'),
|
||||
onResetSession: action('onResetSession'),
|
||||
onSearchInConversation: action('onSearchInConversation'),
|
||||
onSetMuteNotifications: action('onSetMuteNotifications'),
|
||||
onOutgoingAudioCallInConversation: action(
|
||||
|
@ -49,8 +48,6 @@ const commonProps = {
|
|||
'onOutgoingVideoCallInConversation'
|
||||
),
|
||||
|
||||
onShowChatColorEditor: action('onShowChatColorEditor'),
|
||||
onShowSafetyNumber: action('onShowSafetyNumber'),
|
||||
onShowAllMedia: action('onShowAllMedia'),
|
||||
onShowContactModal: action('onShowContactModal'),
|
||||
onShowGroupMembers: action('onShowGroupMembers'),
|
||||
|
|
|
@ -68,15 +68,12 @@ export type PropsActionsType = {
|
|||
onSetDisappearingMessages: (seconds: number) => void;
|
||||
onShowContactModal: (contactId: string) => void;
|
||||
onDeleteMessages: () => void;
|
||||
onResetSession: () => void;
|
||||
onSearchInConversation: () => void;
|
||||
onOutgoingAudioCallInConversation: () => void;
|
||||
onOutgoingVideoCallInConversation: () => void;
|
||||
onSetPin: (value: boolean) => void;
|
||||
|
||||
onShowChatColorEditor: () => void;
|
||||
onShowConversationDetails: () => void;
|
||||
onShowSafetyNumber: () => void;
|
||||
onShowAllMedia: () => void;
|
||||
onShowGroupMembers: () => void;
|
||||
onGoBack: () => void;
|
||||
|
@ -369,32 +366,28 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
|
||||
private renderMenu(triggerId: string): ReactNode {
|
||||
const {
|
||||
i18n,
|
||||
acceptedMessageRequest,
|
||||
canChangeTimer,
|
||||
expireTimer,
|
||||
groupVersion,
|
||||
i18n,
|
||||
isArchived,
|
||||
isMe,
|
||||
isMissingMandatoryProfileSharing,
|
||||
isPinned,
|
||||
type,
|
||||
left,
|
||||
markedUnread,
|
||||
muteExpiresAt,
|
||||
isMissingMandatoryProfileSharing,
|
||||
left,
|
||||
groupVersion,
|
||||
onArchive,
|
||||
onDeleteMessages,
|
||||
onResetSession,
|
||||
onMarkUnread,
|
||||
onMoveToInbox,
|
||||
onSetDisappearingMessages,
|
||||
onSetMuteNotifications,
|
||||
onSetPin,
|
||||
onShowAllMedia,
|
||||
onShowChatColorEditor,
|
||||
onShowConversationDetails,
|
||||
onShowGroupMembers,
|
||||
onShowSafetyNumber,
|
||||
onArchive,
|
||||
onMarkUnread,
|
||||
onSetPin,
|
||||
onMoveToInbox,
|
||||
type,
|
||||
} = this.props;
|
||||
|
||||
const muteOptions = getMuteOptions(muteExpiresAt, i18n);
|
||||
|
@ -484,14 +477,11 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
</MenuItem>
|
||||
))}
|
||||
</SubMenu>
|
||||
{!isGroup ? (
|
||||
<MenuItem onClick={onShowChatColorEditor}>
|
||||
{i18n('showChatColorEditor')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
{hasGV2AdminEnabled ? (
|
||||
{!isGroup || hasGV2AdminEnabled ? (
|
||||
<MenuItem onClick={onShowConversationDetails}>
|
||||
{i18n('showConversationDetails')}
|
||||
{isGroup
|
||||
? i18n('showConversationDetails')
|
||||
: i18n('showConversationDetails--direct')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
{isGroup && !hasGV2AdminEnabled ? (
|
||||
|
@ -500,14 +490,6 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
</MenuItem>
|
||||
) : null}
|
||||
<MenuItem onClick={onShowAllMedia}>{i18n('viewRecentMedia')}</MenuItem>
|
||||
{!isGroup && !isMe ? (
|
||||
<MenuItem onClick={onShowSafetyNumber}>
|
||||
{i18n('showSafetyNumber')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
{!isGroup && acceptedMessageRequest ? (
|
||||
<MenuItem onClick={onResetSession}>{i18n('resetSession')}</MenuItem>
|
||||
) : null}
|
||||
<MenuItem divider />
|
||||
{!markedUnread ? (
|
||||
<MenuItem onClick={onMarkUnread}>{i18n('markUnread')}</MenuItem>
|
||||
|
|
|
@ -61,7 +61,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
i18n,
|
||||
interactionMode: 'keyboard',
|
||||
|
||||
showSafetyNumber: action('onShowSafetyNumber'),
|
||||
showSafetyNumber: action('showSafetyNumber'),
|
||||
|
||||
checkForAccount: action('checkForAccount'),
|
||||
clearSelectedMessage: action('clearSelectedMessage'),
|
||||
|
|
|
@ -46,6 +46,7 @@ const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({
|
|||
hasGroupLink,
|
||||
i18n,
|
||||
isAdmin: false,
|
||||
isGroup: true,
|
||||
loadRecentMediaItems: action('loadRecentMediaItems'),
|
||||
memberships: times(32, i => ({
|
||||
isAdmin: i === 1,
|
||||
|
@ -63,7 +64,7 @@ const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({
|
|||
setDisappearingMessages: action('setDisappearingMessages'),
|
||||
showAllMedia: action('showAllMedia'),
|
||||
showContactModal: action('showContactModal'),
|
||||
showGroupChatColorEditor: action('showGroupChatColorEditor'),
|
||||
showChatColorEditor: action('showChatColorEditor'),
|
||||
showGroupLinkManagement: action('showGroupLinkManagement'),
|
||||
showGroupV2Permissions: action('showGroupV2Permissions'),
|
||||
showConversationNotificationsSettings: action(
|
||||
|
@ -76,10 +77,20 @@ const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({
|
|||
},
|
||||
onBlock: action('onBlock'),
|
||||
onLeave: action('onLeave'),
|
||||
onUnblock: action('onUnblock'),
|
||||
deleteAvatarFromDisk: action('deleteAvatarFromDisk'),
|
||||
replaceAvatar: action('replaceAvatar'),
|
||||
saveAvatarToDisk: action('saveAvatarToDisk'),
|
||||
setMuteExpiration: action('setMuteExpiration'),
|
||||
userAvatarData: [],
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
onOutgoingAudioCallInConversation: action(
|
||||
'onOutgoingAudioCallInConversation'
|
||||
),
|
||||
onOutgoingVideoCallInConversation: action(
|
||||
'onOutgoingVideoCallInConversation'
|
||||
),
|
||||
searchInConversation: action('searchInConversation'),
|
||||
});
|
||||
|
||||
story.add('Basic', () => {
|
||||
|
@ -157,3 +168,7 @@ story.add('Group add with missing capabilities', () => (
|
|||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('1:1', () => (
|
||||
<ConversationDetails {...createProps()} isGroup={false} />
|
||||
));
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import React, { useState, ReactNode } from 'react';
|
||||
|
||||
import { Button, ButtonIconType, ButtonVariant } from '../../Button';
|
||||
import { ConversationType } from '../../../state/ducks/conversations';
|
||||
import { assert } from '../../../util/assert';
|
||||
import { getMutedUntilText } from '../../../util/getMutedUntilText';
|
||||
|
@ -19,7 +20,7 @@ import { PanelSection } from './PanelSection';
|
|||
import { AddGroupMembersModal } from './AddGroupMembersModal';
|
||||
import { ConversationDetailsActions } from './ConversationDetailsActions';
|
||||
import { ConversationDetailsHeader } from './ConversationDetailsHeader';
|
||||
import { ConversationDetailsIcon } from './ConversationDetailsIcon';
|
||||
import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon';
|
||||
import { ConversationDetailsMediaList } from './ConversationDetailsMediaList';
|
||||
import {
|
||||
ConversationDetailsMembershipList,
|
||||
|
@ -33,18 +34,22 @@ import { EditConversationAttributesModal } from './EditConversationAttributesMod
|
|||
import { RequestState } from './util';
|
||||
import { getCustomColorStyle } from '../../../util/getCustomColorStyle';
|
||||
import { ConfirmationDialog } from '../../ConfirmationDialog';
|
||||
import { ConversationNotificationsModal } from './ConversationNotificationsModal';
|
||||
import {
|
||||
AvatarDataType,
|
||||
DeleteAvatarFromDiskActionType,
|
||||
ReplaceAvatarActionType,
|
||||
SaveAvatarToDiskActionType,
|
||||
} from '../../../types/Avatar';
|
||||
import { isMuted } from '../../../util/isMuted';
|
||||
|
||||
enum ModalState {
|
||||
NothingOpen,
|
||||
EditingGroupDescription,
|
||||
EditingGroupTitle,
|
||||
AddingGroupMembers,
|
||||
MuteNotifications,
|
||||
UnmuteNotifications,
|
||||
}
|
||||
|
||||
export type StateProps = {
|
||||
|
@ -55,13 +60,14 @@ export type StateProps = {
|
|||
hasGroupLink: boolean;
|
||||
i18n: LocalizerType;
|
||||
isAdmin: boolean;
|
||||
isGroup: boolean;
|
||||
loadRecentMediaItems: (limit: number) => void;
|
||||
memberships: Array<GroupV2Membership>;
|
||||
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
|
||||
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
||||
setDisappearingMessages: (seconds: number) => void;
|
||||
showAllMedia: () => void;
|
||||
showGroupChatColorEditor: () => void;
|
||||
showChatColorEditor: () => void;
|
||||
showGroupLinkManagement: () => void;
|
||||
showGroupV2Permissions: () => void;
|
||||
showPendingInvites: () => void;
|
||||
|
@ -79,7 +85,11 @@ export type StateProps = {
|
|||
) => Promise<void>;
|
||||
onBlock: () => void;
|
||||
onLeave: () => void;
|
||||
onUnblock: () => void;
|
||||
userAvatarData: Array<AvatarDataType>;
|
||||
setMuteExpiration: (muteExpiresAt: undefined | number) => unknown;
|
||||
onOutgoingAudioCallInConversation: () => unknown;
|
||||
onOutgoingVideoCallInConversation: () => unknown;
|
||||
};
|
||||
|
||||
type ActionProps = {
|
||||
|
@ -87,6 +97,8 @@ type ActionProps = {
|
|||
replaceAvatar: ReplaceAvatarActionType;
|
||||
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||
showContactModal: (contactId: string, conversationId: string) => void;
|
||||
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
||||
searchInConversation: (id: string, title: string) => unknown;
|
||||
};
|
||||
|
||||
export type Props = StateProps & ActionProps;
|
||||
|
@ -96,28 +108,35 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
canEditGroupInfo,
|
||||
candidateContactsToAdd,
|
||||
conversation,
|
||||
deleteAvatarFromDisk,
|
||||
hasGroupLink,
|
||||
i18n,
|
||||
isAdmin,
|
||||
isGroup,
|
||||
loadRecentMediaItems,
|
||||
memberships,
|
||||
pendingApprovalMemberships,
|
||||
pendingMemberships,
|
||||
setDisappearingMessages,
|
||||
showAllMedia,
|
||||
showContactModal,
|
||||
showGroupChatColorEditor,
|
||||
showGroupLinkManagement,
|
||||
showGroupV2Permissions,
|
||||
showPendingInvites,
|
||||
showLightboxForMedia,
|
||||
showConversationNotificationsSettings,
|
||||
updateGroupAttributes,
|
||||
onBlock,
|
||||
onLeave,
|
||||
deleteAvatarFromDisk,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
onUnblock,
|
||||
pendingApprovalMemberships,
|
||||
pendingMemberships,
|
||||
replaceAvatar,
|
||||
saveAvatarToDisk,
|
||||
searchInConversation,
|
||||
setDisappearingMessages,
|
||||
setMuteExpiration,
|
||||
showAllMedia,
|
||||
showChatColorEditor,
|
||||
showContactModal,
|
||||
showConversationNotificationsSettings,
|
||||
showGroupLinkManagement,
|
||||
showGroupV2Permissions,
|
||||
showLightboxForMedia,
|
||||
showPendingInvites,
|
||||
toggleSafetyNumberModal,
|
||||
updateGroupAttributes,
|
||||
userAvatarData,
|
||||
}) => {
|
||||
const [modalState, setModalState] = useState<ModalState>(
|
||||
|
@ -241,10 +260,45 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
/>
|
||||
);
|
||||
break;
|
||||
case ModalState.MuteNotifications:
|
||||
modalNode = (
|
||||
<ConversationNotificationsModal
|
||||
i18n={i18n}
|
||||
muteExpiresAt={conversation.muteExpiresAt}
|
||||
onClose={() => {
|
||||
setModalState(ModalState.NothingOpen);
|
||||
}}
|
||||
setMuteExpiration={setMuteExpiration}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case ModalState.UnmuteNotifications:
|
||||
modalNode = (
|
||||
<ConfirmationDialog
|
||||
actions={[
|
||||
{
|
||||
action: () => setMuteExpiration(0),
|
||||
style: 'affirmative',
|
||||
text: i18n('unmute'),
|
||||
},
|
||||
]}
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
title={i18n('ConversationDetails__unmute--title')}
|
||||
onClose={() => {
|
||||
setModalState(ModalState.NothingOpen);
|
||||
}}
|
||||
>
|
||||
{getMutedUntilText(Number(conversation.muteExpiresAt), i18n)}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(modalState);
|
||||
}
|
||||
|
||||
const isConversationMuted = isMuted(conversation.muteExpiresAt);
|
||||
|
||||
return (
|
||||
<div className="conversation-details-panel">
|
||||
{membersMissingCapability && (
|
||||
|
@ -261,6 +315,8 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
canEdit={canEditGroupInfo}
|
||||
conversation={conversation}
|
||||
i18n={i18n}
|
||||
isMe={conversation.isMe}
|
||||
isGroup={isGroup}
|
||||
memberships={memberships}
|
||||
startEditing={(isGroupTitle: boolean) => {
|
||||
setModalState(
|
||||
|
@ -271,15 +327,65 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
}}
|
||||
/>
|
||||
|
||||
<div className="ConversationDetails__header-buttons">
|
||||
{!conversation.isMe && (
|
||||
<>
|
||||
<Button
|
||||
icon={ButtonIconType.video}
|
||||
onClick={onOutgoingVideoCallInConversation}
|
||||
variant={ButtonVariant.Details}
|
||||
>
|
||||
{i18n('video')}
|
||||
</Button>
|
||||
{!isGroup && (
|
||||
<Button
|
||||
icon={ButtonIconType.audio}
|
||||
onClick={onOutgoingAudioCallInConversation}
|
||||
variant={ButtonVariant.Details}
|
||||
>
|
||||
{i18n('audio')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
icon={
|
||||
isConversationMuted ? ButtonIconType.muted : ButtonIconType.unmuted
|
||||
}
|
||||
onClick={() => {
|
||||
if (isConversationMuted) {
|
||||
setModalState(ModalState.UnmuteNotifications);
|
||||
} else {
|
||||
setModalState(ModalState.MuteNotifications);
|
||||
}
|
||||
}}
|
||||
variant={ButtonVariant.Details}
|
||||
>
|
||||
{isConversationMuted ? i18n('unmute') : i18n('mute')}
|
||||
</Button>
|
||||
<Button
|
||||
icon={ButtonIconType.search}
|
||||
onClick={() => {
|
||||
searchInConversation(
|
||||
conversation.id,
|
||||
conversation.isMe ? i18n('noteToSelf') : conversation.title
|
||||
);
|
||||
}}
|
||||
variant={ButtonVariant.Details}
|
||||
>
|
||||
{i18n('search')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<PanelSection>
|
||||
{canEditGroupInfo ? (
|
||||
{!isGroup || canEditGroupInfo ? (
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n(
|
||||
'ConversationDetails--disappearing-messages-label'
|
||||
)}
|
||||
icon="timer"
|
||||
icon={IconType.timer}
|
||||
/>
|
||||
}
|
||||
info={i18n('ConversationDetails--disappearing-messages-info')}
|
||||
|
@ -297,86 +403,110 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('showChatColorEditor')}
|
||||
icon="color"
|
||||
icon={IconType.color}
|
||||
/>
|
||||
}
|
||||
label={i18n('showChatColorEditor')}
|
||||
onClick={showGroupChatColorEditor}
|
||||
onClick={showChatColorEditor}
|
||||
right={
|
||||
<div
|
||||
className={`module-conversation-details__chat-color module-conversation-details__chat-color--${conversation.conversationColor}`}
|
||||
className={`ConversationDetails__chat-color ConversationDetails__chat-color--${conversation.conversationColor}`}
|
||||
style={{
|
||||
...getCustomColorStyle(conversation.customColor),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('ConversationDetails--notifications')}
|
||||
icon="notifications"
|
||||
/>
|
||||
}
|
||||
label={i18n('ConversationDetails--notifications')}
|
||||
onClick={showConversationNotificationsSettings}
|
||||
right={
|
||||
conversation.muteExpiresAt
|
||||
? getMutedUntilText(conversation.muteExpiresAt, i18n)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</PanelSection>
|
||||
|
||||
<ConversationDetailsMembershipList
|
||||
canAddNewMembers={canEditGroupInfo}
|
||||
conversationId={conversation.id}
|
||||
i18n={i18n}
|
||||
memberships={memberships}
|
||||
showContactModal={showContactModal}
|
||||
startAddingNewMembers={() => {
|
||||
setModalState(ModalState.AddingGroupMembers);
|
||||
}}
|
||||
/>
|
||||
|
||||
<PanelSection>
|
||||
{isAdmin || hasGroupLink ? (
|
||||
{isGroup && (
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('ConversationDetails--group-link')}
|
||||
icon="link"
|
||||
ariaLabel={i18n('ConversationDetails--notifications')}
|
||||
icon={IconType.notifications}
|
||||
/>
|
||||
}
|
||||
label={i18n('ConversationDetails--group-link')}
|
||||
onClick={showGroupLinkManagement}
|
||||
right={hasGroupLink ? i18n('on') : i18n('off')}
|
||||
label={i18n('ConversationDetails--notifications')}
|
||||
onClick={showConversationNotificationsSettings}
|
||||
right={
|
||||
conversation.muteExpiresAt
|
||||
? getMutedUntilText(conversation.muteExpiresAt, i18n)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('ConversationDetails--requests-and-invites')}
|
||||
icon="invites"
|
||||
)}
|
||||
{!isGroup && !conversation.isMe && (
|
||||
<>
|
||||
<PanelRow
|
||||
onClick={() => toggleSafetyNumberModal(conversation.id)}
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('verifyNewNumber')}
|
||||
icon={IconType.verify}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<div className="ConversationDetails__safety-number">
|
||||
{i18n('verifyNewNumber')}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
label={i18n('ConversationDetails--requests-and-invites')}
|
||||
onClick={showPendingInvites}
|
||||
right={invitesCount}
|
||||
</>
|
||||
)}
|
||||
</PanelSection>
|
||||
|
||||
{isGroup && (
|
||||
<ConversationDetailsMembershipList
|
||||
canAddNewMembers={canEditGroupInfo}
|
||||
conversationId={conversation.id}
|
||||
i18n={i18n}
|
||||
memberships={memberships}
|
||||
showContactModal={showContactModal}
|
||||
startAddingNewMembers={() => {
|
||||
setModalState(ModalState.AddingGroupMembers);
|
||||
}}
|
||||
/>
|
||||
{isAdmin ? (
|
||||
)}
|
||||
|
||||
{isGroup && (
|
||||
<PanelSection>
|
||||
{isAdmin || hasGroupLink ? (
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('ConversationDetails--group-link')}
|
||||
icon={IconType.link}
|
||||
/>
|
||||
}
|
||||
label={i18n('ConversationDetails--group-link')}
|
||||
onClick={showGroupLinkManagement}
|
||||
right={hasGroupLink ? i18n('on') : i18n('off')}
|
||||
/>
|
||||
) : null}
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('permissions')}
|
||||
icon="lock"
|
||||
ariaLabel={i18n('ConversationDetails--requests-and-invites')}
|
||||
icon={IconType.invites}
|
||||
/>
|
||||
}
|
||||
label={i18n('permissions')}
|
||||
onClick={showGroupV2Permissions}
|
||||
label={i18n('ConversationDetails--requests-and-invites')}
|
||||
onClick={showPendingInvites}
|
||||
right={invitesCount}
|
||||
/>
|
||||
) : null}
|
||||
</PanelSection>
|
||||
{isAdmin ? (
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('permissions')}
|
||||
icon={IconType.lock}
|
||||
/>
|
||||
}
|
||||
label={i18n('permissions')}
|
||||
onClick={showGroupV2Permissions}
|
||||
/>
|
||||
) : null}
|
||||
</PanelSection>
|
||||
)}
|
||||
|
||||
<ConversationDetailsMediaList
|
||||
conversation={conversation}
|
||||
|
@ -386,14 +516,19 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
showLightboxForMedia={showLightboxForMedia}
|
||||
/>
|
||||
|
||||
<ConversationDetailsActions
|
||||
i18n={i18n}
|
||||
cannotLeaveBecauseYouAreLastAdmin={cannotLeaveBecauseYouAreLastAdmin}
|
||||
conversationTitle={conversation.title}
|
||||
left={Boolean(conversation.left)}
|
||||
onLeave={onLeave}
|
||||
onBlock={onBlock}
|
||||
/>
|
||||
{!conversation.isMe && (
|
||||
<ConversationDetailsActions
|
||||
cannotLeaveBecauseYouAreLastAdmin={cannotLeaveBecauseYouAreLastAdmin}
|
||||
conversationTitle={conversation.title}
|
||||
i18n={i18n}
|
||||
isBlocked={Boolean(conversation.isBlocked)}
|
||||
isGroup={isGroup}
|
||||
left={Boolean(conversation.left)}
|
||||
onBlock={onBlock}
|
||||
onLeave={onLeave}
|
||||
onUnblock={onUnblock}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalNode}
|
||||
</div>
|
||||
|
|
|
@ -31,7 +31,10 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
left: isBoolean(overrideProps.left) ? overrideProps.left : false,
|
||||
onBlock: action('onBlock'),
|
||||
onLeave: action('onLeave'),
|
||||
onUnblock: action('onUnblock'),
|
||||
i18n,
|
||||
isBlocked: false,
|
||||
isGroup: true,
|
||||
});
|
||||
|
||||
story.add('Basic', () => {
|
||||
|
@ -51,3 +54,11 @@ story.add('Cannot leave because you are the last admin', () => {
|
|||
|
||||
return <ConversationDetailsActions {...props} />;
|
||||
});
|
||||
|
||||
story.add('1:1', () => (
|
||||
<ConversationDetailsActions {...createProps()} isGroup={false} />
|
||||
));
|
||||
|
||||
story.add('1:1 Blocked', () => (
|
||||
<ConversationDetailsActions {...createProps()} isGroup={false} isBlocked />
|
||||
));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { LocalizerType } from '../../../types/Util';
|
||||
|
@ -10,48 +10,55 @@ import { Tooltip, TooltipPlacement } from '../../Tooltip';
|
|||
|
||||
import { PanelRow } from './PanelRow';
|
||||
import { PanelSection } from './PanelSection';
|
||||
import { ConversationDetailsIcon } from './ConversationDetailsIcon';
|
||||
import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon';
|
||||
|
||||
export type Props = {
|
||||
cannotLeaveBecauseYouAreLastAdmin: boolean;
|
||||
conversationTitle: string;
|
||||
i18n: LocalizerType;
|
||||
isBlocked: boolean;
|
||||
isGroup: boolean;
|
||||
left: boolean;
|
||||
onBlock: () => void;
|
||||
onLeave: () => void;
|
||||
i18n: LocalizerType;
|
||||
onUnblock: () => void;
|
||||
};
|
||||
|
||||
export const ConversationDetailsActions: React.ComponentType<Props> = ({
|
||||
cannotLeaveBecauseYouAreLastAdmin,
|
||||
conversationTitle,
|
||||
i18n,
|
||||
isBlocked,
|
||||
isGroup,
|
||||
left,
|
||||
onBlock,
|
||||
onLeave,
|
||||
i18n,
|
||||
onUnblock,
|
||||
}) => {
|
||||
const [confirmingLeave, setConfirmingLeave] = React.useState<boolean>(false);
|
||||
const [confirmingBlock, setConfirmingBlock] = React.useState<boolean>(false);
|
||||
const [confirmLeave, gLeave] = useState<boolean>(false);
|
||||
const [confirmGroupBlock, gGroupBlock] = useState<boolean>(false);
|
||||
const [confirmDirectBlock, gDirectBlock] = useState<boolean>(false);
|
||||
const [confirmDirectUnblock, gDirectUnblock] = useState<boolean>(false);
|
||||
|
||||
let leaveGroupNode: ReactNode;
|
||||
let blockGroupNode: ReactNode;
|
||||
if (!left) {
|
||||
if (isGroup && !left) {
|
||||
leaveGroupNode = (
|
||||
<PanelRow
|
||||
disabled={cannotLeaveBecauseYouAreLastAdmin}
|
||||
onClick={() => setConfirmingLeave(true)}
|
||||
onClick={() => gLeave(true)}
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('ConversationDetailsActions--leave-group')}
|
||||
disabled={cannotLeaveBecauseYouAreLastAdmin}
|
||||
icon="leave"
|
||||
icon={IconType.leave}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<div
|
||||
className={classNames(
|
||||
'module-conversation-details__leave-group',
|
||||
'ConversationDetails__leave-group',
|
||||
cannotLeaveBecauseYouAreLastAdmin &&
|
||||
'module-conversation-details__leave-group--disabled'
|
||||
'ConversationDetails__leave-group--disabled'
|
||||
)}
|
||||
>
|
||||
{i18n('ConversationDetailsActions--leave-group')}
|
||||
|
@ -73,32 +80,49 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
|
|||
}
|
||||
}
|
||||
|
||||
blockGroupNode = (
|
||||
<PanelRow
|
||||
disabled={cannotLeaveBecauseYouAreLastAdmin}
|
||||
onClick={() => setConfirmingBlock(true)}
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('ConversationDetailsActions--block-group')}
|
||||
icon="block"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<div className="module-conversation-details__block-group">
|
||||
{i18n('ConversationDetailsActions--block-group')}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
let blockNode: ReactNode;
|
||||
if (isGroup) {
|
||||
blockNode = (
|
||||
<PanelRow
|
||||
disabled={cannotLeaveBecauseYouAreLastAdmin}
|
||||
onClick={() => gGroupBlock(true)}
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('ConversationDetailsActions--block-group')}
|
||||
icon={IconType.block}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<div className="ConversationDetails__block-group">
|
||||
{i18n('ConversationDetailsActions--block-group')}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const label = isBlocked
|
||||
? i18n('MessageRequests--unblock')
|
||||
: i18n('MessageRequests--block');
|
||||
blockNode = (
|
||||
<PanelRow
|
||||
onClick={() => (isBlocked ? gDirectUnblock(true) : gDirectBlock(true))}
|
||||
icon={
|
||||
<ConversationDetailsIcon ariaLabel={label} icon={IconType.block} />
|
||||
}
|
||||
label={<div className="ConversationDetails__block-group">{label}</div>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (cannotLeaveBecauseYouAreLastAdmin) {
|
||||
blockGroupNode = (
|
||||
blockNode = (
|
||||
<Tooltip
|
||||
content={i18n(
|
||||
'ConversationDetailsActions--leave-group-must-choose-new-admin'
|
||||
)}
|
||||
direction={TooltipPlacement.Top}
|
||||
>
|
||||
{blockGroupNode}
|
||||
{blockNode}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
@ -107,10 +131,10 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
|
|||
<>
|
||||
<PanelSection>
|
||||
{leaveGroupNode}
|
||||
{blockGroupNode}
|
||||
{blockNode}
|
||||
</PanelSection>
|
||||
|
||||
{confirmingLeave && (
|
||||
{confirmLeave && (
|
||||
<ConfirmationDialog
|
||||
actions={[
|
||||
{
|
||||
|
@ -122,14 +146,14 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
|
|||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={() => setConfirmingLeave(false)}
|
||||
onClose={() => gLeave(false)}
|
||||
title={i18n('ConversationDetailsActions--leave-group-modal-title')}
|
||||
>
|
||||
{i18n('ConversationDetailsActions--leave-group-modal-content')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
{confirmingBlock && (
|
||||
{confirmGroupBlock && (
|
||||
<ConfirmationDialog
|
||||
actions={[
|
||||
{
|
||||
|
@ -141,7 +165,7 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
|
|||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={() => setConfirmingBlock(false)}
|
||||
onClose={() => gGroupBlock(false)}
|
||||
title={i18n('ConversationDetailsActions--block-group-modal-title', [
|
||||
conversationTitle,
|
||||
])}
|
||||
|
@ -149,6 +173,44 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
|
|||
{i18n('ConversationDetailsActions--block-group-modal-content')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
{confirmDirectBlock && (
|
||||
<ConfirmationDialog
|
||||
actions={[
|
||||
{
|
||||
text: i18n('MessageRequests--block'),
|
||||
action: onBlock,
|
||||
style: 'affirmative',
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={() => gDirectBlock(false)}
|
||||
title={i18n('MessageRequests--block-direct-confirm-title', [
|
||||
conversationTitle,
|
||||
])}
|
||||
>
|
||||
{i18n('MessageRequests--block-direct-confirm-body')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
{confirmDirectUnblock && (
|
||||
<ConfirmationDialog
|
||||
actions={[
|
||||
{
|
||||
text: i18n('MessageRequests--unblock'),
|
||||
action: onUnblock,
|
||||
style: 'affirmative',
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={() => gDirectUnblock(false)}
|
||||
title={i18n('MessageRequests--unblock-direct-confirm-title', [
|
||||
conversationTitle,
|
||||
])}
|
||||
>
|
||||
{i18n('MessageRequests--unblock-direct-confirm-body')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -36,6 +36,8 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
canEdit: false,
|
||||
startEditing: action('startEditing'),
|
||||
memberships: new Array(number('conversation members length', 0)),
|
||||
isGroup: false,
|
||||
isMe: false,
|
||||
...overrideProps,
|
||||
});
|
||||
|
||||
|
@ -78,3 +80,11 @@ story.add('Editable no-description', () => {
|
|||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('1:1', () => (
|
||||
<ConversationDetailsHeader {...createProps()} isGroup={false} />
|
||||
));
|
||||
|
||||
story.add('Note to self', () => (
|
||||
<ConversationDetailsHeader {...createProps()} isMe />
|
||||
));
|
||||
|
|
|
@ -16,36 +16,44 @@ export type Props = {
|
|||
canEdit: boolean;
|
||||
conversation: ConversationType;
|
||||
i18n: LocalizerType;
|
||||
isGroup: boolean;
|
||||
isMe: boolean;
|
||||
memberships: Array<GroupV2Membership>;
|
||||
startEditing: (isGroupTitle: boolean) => void;
|
||||
};
|
||||
|
||||
const bem = bemGenerator('module-conversation-details-header');
|
||||
const bem = bemGenerator('ConversationDetails-header');
|
||||
|
||||
export const ConversationDetailsHeader: React.ComponentType<Props> = ({
|
||||
canEdit,
|
||||
conversation,
|
||||
i18n,
|
||||
isGroup,
|
||||
isMe,
|
||||
memberships,
|
||||
startEditing,
|
||||
}) => {
|
||||
const [showingAvatar, setShowingAvatar] = useState(false);
|
||||
|
||||
let subtitle: ReactNode;
|
||||
if (conversation.groupDescription) {
|
||||
subtitle = (
|
||||
<GroupDescription
|
||||
i18n={i18n}
|
||||
text={conversation.groupDescription}
|
||||
title={conversation.title}
|
||||
/>
|
||||
);
|
||||
} else if (canEdit) {
|
||||
subtitle = i18n('ConversationDetailsHeader--add-group-description');
|
||||
} else {
|
||||
subtitle = i18n('ConversationDetailsHeader--members', [
|
||||
memberships.length.toString(),
|
||||
]);
|
||||
if (isGroup) {
|
||||
if (conversation.groupDescription) {
|
||||
subtitle = (
|
||||
<GroupDescription
|
||||
i18n={i18n}
|
||||
text={conversation.groupDescription}
|
||||
title={conversation.title}
|
||||
/>
|
||||
);
|
||||
} else if (canEdit) {
|
||||
subtitle = i18n('ConversationDetailsHeader--add-group-description');
|
||||
} else {
|
||||
subtitle = i18n('ConversationDetailsHeader--members', [
|
||||
memberships.length.toString(),
|
||||
]);
|
||||
}
|
||||
} else if (!isMe) {
|
||||
subtitle = conversation.phoneNumber;
|
||||
}
|
||||
|
||||
const avatar = (
|
||||
|
@ -54,6 +62,7 @@ export const ConversationDetailsHeader: React.ComponentType<Props> = ({
|
|||
i18n={i18n}
|
||||
size={80}
|
||||
{...conversation}
|
||||
noteToSelf={isMe}
|
||||
onClick={() => setShowingAvatar(true)}
|
||||
sharedGroupNames={[]}
|
||||
/>
|
||||
|
@ -62,21 +71,22 @@ export const ConversationDetailsHeader: React.ComponentType<Props> = ({
|
|||
const contents = (
|
||||
<div>
|
||||
<div className={bem('title')}>
|
||||
<Emojify text={conversation.title} />
|
||||
<Emojify text={isMe ? i18n('noteToSelf') : conversation.title} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const avatarLightbox = showingAvatar ? (
|
||||
<AvatarLightbox
|
||||
avatarColor={conversation.color}
|
||||
avatarPath={conversation.avatarPath}
|
||||
conversationTitle={conversation.title}
|
||||
i18n={i18n}
|
||||
isGroup
|
||||
onClose={() => setShowingAvatar(false)}
|
||||
/>
|
||||
) : null;
|
||||
const avatarLightbox =
|
||||
showingAvatar && !isMe ? (
|
||||
<AvatarLightbox
|
||||
avatarColor={conversation.color}
|
||||
avatarPath={conversation.avatarPath}
|
||||
conversationTitle={conversation.title}
|
||||
i18n={i18n}
|
||||
isGroup
|
||||
onClose={() => setShowingAvatar(false)}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
if (canEdit) {
|
||||
return (
|
||||
|
|
|
@ -6,7 +6,11 @@ import * as React from 'react';
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { ConversationDetailsIcon, Props } from './ConversationDetailsIcon';
|
||||
import {
|
||||
ConversationDetailsIcon,
|
||||
Props,
|
||||
IconType,
|
||||
} from './ConversationDetailsIcon';
|
||||
|
||||
const story = storiesOf(
|
||||
'Components/Conversation/ConversationDetails/ConversationDetailIcon',
|
||||
|
@ -15,12 +19,12 @@ const story = storiesOf(
|
|||
|
||||
const createProps = (overrideProps: Partial<Props>): Props => ({
|
||||
ariaLabel: overrideProps.ariaLabel || '',
|
||||
icon: overrideProps.icon || '',
|
||||
icon: overrideProps.icon || IconType.timer,
|
||||
onClick: overrideProps.onClick,
|
||||
});
|
||||
|
||||
story.add('All', () => {
|
||||
const icons = ['timer', 'trash', 'invites', 'block', 'leave', 'down'];
|
||||
const icons = Object.values(IconType);
|
||||
|
||||
return icons.map(icon => (
|
||||
<ConversationDetailsIcon {...createProps({ icon })} />
|
||||
|
@ -28,7 +32,14 @@ story.add('All', () => {
|
|||
});
|
||||
|
||||
story.add('Clickable Icons', () => {
|
||||
const icons = ['timer', 'trash', 'invites', 'block', 'leave', 'down'];
|
||||
const icons = [
|
||||
IconType.timer,
|
||||
IconType.trash,
|
||||
IconType.invites,
|
||||
IconType.block,
|
||||
IconType.leave,
|
||||
IconType.down,
|
||||
];
|
||||
|
||||
const onClick = action('onClick');
|
||||
|
||||
|
|
|
@ -6,14 +6,32 @@ import classNames from 'classnames';
|
|||
|
||||
import { bemGenerator } from './util';
|
||||
|
||||
export enum IconType {
|
||||
'block' = 'block',
|
||||
'color' = 'color',
|
||||
'down' = 'down',
|
||||
'invites' = 'invites',
|
||||
'leave' = 'leave',
|
||||
'link' = 'link',
|
||||
'lock' = 'lock',
|
||||
'mention' = 'mention',
|
||||
'mute' = 'mute',
|
||||
'notifications' = 'notifications',
|
||||
'reset' = 'reset',
|
||||
'share' = 'share',
|
||||
'timer' = 'timer',
|
||||
'trash' = 'trash',
|
||||
'verify' = 'verify',
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
ariaLabel: string;
|
||||
disabled?: boolean;
|
||||
icon: string;
|
||||
icon: IconType;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
const bem = bemGenerator('module-conversation-details-icon');
|
||||
const bem = bemGenerator('ConversationDetails-icon');
|
||||
|
||||
export const ConversationDetailsIcon: React.ComponentType<Props> = ({
|
||||
ariaLabel,
|
||||
|
|
|
@ -25,7 +25,7 @@ export type Props = {
|
|||
|
||||
const MEDIA_ITEM_LIMIT = 6;
|
||||
|
||||
const bem = bemGenerator('module-conversation-details-media-list');
|
||||
const bem = bemGenerator('ConversationDetails-media-list');
|
||||
|
||||
export const ConversationDetailsMediaList: React.ComponentType<Props> = ({
|
||||
conversation,
|
||||
|
@ -36,11 +36,13 @@ export const ConversationDetailsMediaList: React.ComponentType<Props> = ({
|
|||
}) => {
|
||||
const mediaItems = conversation.recentMediaItems || [];
|
||||
|
||||
const mediaItemsLength = mediaItems.length;
|
||||
|
||||
React.useEffect(() => {
|
||||
loadRecentMediaItems(MEDIA_ITEM_LIMIT);
|
||||
}, [loadRecentMediaItems]);
|
||||
}, [loadRecentMediaItems, mediaItemsLength]);
|
||||
|
||||
if (mediaItems.length === 0) {
|
||||
if (mediaItemsLength === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import { LocalizerType } from '../../../types/Util';
|
|||
import { Avatar } from '../../Avatar';
|
||||
import { Emojify } from '../Emojify';
|
||||
|
||||
import { ConversationDetailsIcon } from './ConversationDetailsIcon';
|
||||
import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon';
|
||||
import { ConversationType } from '../../../state/ducks/conversations';
|
||||
import { PanelRow } from './PanelRow';
|
||||
import { PanelSection } from './PanelSection';
|
||||
|
@ -94,7 +94,7 @@ export const ConversationDetailsMembershipList: React.ComponentType<Props> = ({
|
|||
{canAddNewMembers && (
|
||||
<PanelRow
|
||||
icon={
|
||||
<div className="module-conversation-details-membership-list__add-members-icon" />
|
||||
<div className="ConversationDetails-membership-list__add-members-icon" />
|
||||
}
|
||||
label={i18n('ConversationDetailsMembershipList--add-members')}
|
||||
onClick={() => startAddingNewMembers?.()}
|
||||
|
@ -118,11 +118,11 @@ export const ConversationDetailsMembershipList: React.ComponentType<Props> = ({
|
|||
))}
|
||||
{showAllMembers === false && shouldHideRestMembers && (
|
||||
<PanelRow
|
||||
className="module-conversation-details-membership-list--show-all"
|
||||
className="ConversationDetails-membership-list--show-all"
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('ConversationDetailsMembershipList--show-all')}
|
||||
icon="down"
|
||||
icon={IconType.down}
|
||||
/>
|
||||
}
|
||||
onClick={() => setShowAllMembers(true)}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { LocalizerType } from '../../../types/Util';
|
||||
import { getMuteOptions } from '../../../util/getMuteOptions';
|
||||
import { parseIntOrThrow } from '../../../util/parseIntOrThrow';
|
||||
import { Checkbox } from '../../Checkbox';
|
||||
import { Modal } from '../../Modal';
|
||||
import { Button, ButtonVariant } from '../../Button';
|
||||
|
||||
type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
muteExpiresAt: undefined | number;
|
||||
onClose: () => unknown;
|
||||
setMuteExpiration: (muteExpiresAt: undefined | number) => unknown;
|
||||
};
|
||||
|
||||
export const ConversationNotificationsModal = ({
|
||||
i18n,
|
||||
muteExpiresAt,
|
||||
onClose,
|
||||
setMuteExpiration,
|
||||
}: PropsType): JSX.Element => {
|
||||
const muteOptions = useMemo(
|
||||
() =>
|
||||
getMuteOptions(muteExpiresAt, i18n).map(({ disabled, name, value }) => ({
|
||||
disabled,
|
||||
text: name,
|
||||
value,
|
||||
})),
|
||||
[i18n, muteExpiresAt]
|
||||
);
|
||||
|
||||
const [muteExpirationValue, setMuteExpirationValue] = useState(muteExpiresAt);
|
||||
|
||||
const onMuteChange = () => {
|
||||
const ms = parseIntOrThrow(
|
||||
muteExpirationValue,
|
||||
'NotificationSettings: mute ms was not an integer'
|
||||
);
|
||||
setMuteExpiration(ms);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
hasStickyButtons
|
||||
hasXButton
|
||||
onClose={onClose}
|
||||
i18n={i18n}
|
||||
title={i18n('muteNotificationsTitle')}
|
||||
>
|
||||
{muteOptions
|
||||
.filter(x => x.value > 0)
|
||||
.map(option => (
|
||||
<Checkbox
|
||||
checked={muteExpirationValue === option.value}
|
||||
disabled={option.disabled}
|
||||
isRadio
|
||||
label={option.text}
|
||||
moduleClassName="ConversationDetails__radio"
|
||||
name="mute"
|
||||
onChange={value => value && setMuteExpirationValue(option.value)}
|
||||
/>
|
||||
))}
|
||||
<Modal.ButtonFooter>
|
||||
<Button onClick={onClose} variant={ButtonVariant.Secondary}>
|
||||
{i18n('cancel')}
|
||||
</Button>
|
||||
<Button onClick={onMuteChange} variant={ButtonVariant.Primary}>
|
||||
{i18n('mute')}
|
||||
</Button>
|
||||
</Modal.ButtonFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -7,10 +7,9 @@ import { ConversationTypeType } from '../../../state/ducks/conversations';
|
|||
import { LocalizerType } from '../../../types/Util';
|
||||
import { PanelSection } from './PanelSection';
|
||||
import { PanelRow } from './PanelRow';
|
||||
import { ConversationDetailsIcon } from './ConversationDetailsIcon';
|
||||
import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon';
|
||||
import { Select } from '../../Select';
|
||||
import { isMuted } from '../../../util/isMuted';
|
||||
import { assert } from '../../../util/assert';
|
||||
import { getMuteOptions } from '../../../util/getMuteOptions';
|
||||
import { parseIntOrThrow } from '../../../util/parseIntOrThrow';
|
||||
|
||||
|
@ -33,13 +32,6 @@ export const ConversationNotificationsSettings: FunctionComponent<PropsType> = (
|
|||
setMuteExpiration,
|
||||
setDontNotifyForMentionsIfMuted,
|
||||
}) => {
|
||||
// This assertion is here to prevent accidental usage of this component in an untested
|
||||
// context.
|
||||
assert(
|
||||
conversationType === 'group',
|
||||
'<ConversationNotificationsSettings> SHOULD work for non-group conversations, but it has not been tested there'
|
||||
);
|
||||
|
||||
const muteOptions = useMemo(
|
||||
() => [
|
||||
...(isMuted(muteExpiresAt)
|
||||
|
@ -81,7 +73,7 @@ export const ConversationNotificationsSettings: FunctionComponent<PropsType> = (
|
|||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('muteNotificationsTitle')}
|
||||
icon="mute"
|
||||
icon={IconType.mute}
|
||||
/>
|
||||
}
|
||||
label={i18n('muteNotificationsTitle')}
|
||||
|
@ -96,7 +88,7 @@ export const ConversationNotificationsSettings: FunctionComponent<PropsType> = (
|
|||
ariaLabel={i18n(
|
||||
'ConversationNotificationsSettings__mentions__label'
|
||||
)}
|
||||
icon="mention"
|
||||
icon={IconType.mention}
|
||||
/>
|
||||
}
|
||||
label={i18n('ConversationNotificationsSettings__mentions__label')}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { ConversationDetailsIcon } from './ConversationDetailsIcon';
|
||||
import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon';
|
||||
import { SignalService as Proto } from '../../../protobuf';
|
||||
import { ConversationType } from '../../../state/ducks/conversations';
|
||||
import { LocalizerType } from '../../../types/Util';
|
||||
|
@ -86,7 +86,7 @@ export const GroupLinkManagement: React.ComponentType<PropsType> = ({
|
|||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('GroupLinkManagement--share')}
|
||||
icon="share"
|
||||
icon={IconType.share}
|
||||
/>
|
||||
}
|
||||
label={i18n('GroupLinkManagement--share')}
|
||||
|
@ -101,7 +101,7 @@ export const GroupLinkManagement: React.ComponentType<PropsType> = ({
|
|||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('GroupLinkManagement--reset')}
|
||||
icon="reset"
|
||||
icon={IconType.reset}
|
||||
/>
|
||||
}
|
||||
label={i18n('GroupLinkManagement--reset')}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { storiesOf } from '@storybook/react';
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import { boolean, text } from '@storybook/addon-knobs';
|
||||
|
||||
import { ConversationDetailsIcon } from './ConversationDetailsIcon';
|
||||
import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon';
|
||||
import { PanelRow, Props } from './PanelRow';
|
||||
|
||||
const story = storiesOf(
|
||||
|
@ -17,7 +17,7 @@ const story = storiesOf(
|
|||
|
||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
icon: boolean('with icon', overrideProps.icon !== undefined) ? (
|
||||
<ConversationDetailsIcon ariaLabel="timer" icon="timer" />
|
||||
<ConversationDetailsIcon ariaLabel="timer" icon={IconType.timer} />
|
||||
) : null,
|
||||
label: text('label', (overrideProps.label as string) || ''),
|
||||
info: text('info', overrideProps.info || ''),
|
||||
|
@ -25,7 +25,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
actions: boolean('with action', overrideProps.actions !== undefined) ? (
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel="trash"
|
||||
icon="trash"
|
||||
icon={IconType.trash}
|
||||
onClick={action('action onClick')}
|
||||
/>
|
||||
) : null,
|
||||
|
|
|
@ -17,7 +17,7 @@ export type Props = {
|
|||
onClick?: () => void;
|
||||
};
|
||||
|
||||
const bem = bemGenerator('module-conversation-details-panel-row');
|
||||
const bem = bemGenerator('ConversationDetails-panel-row');
|
||||
|
||||
export const PanelRow: React.ComponentType<Props> = ({
|
||||
alwaysShowActions,
|
||||
|
|
|
@ -12,7 +12,7 @@ export type Props = {
|
|||
title?: string;
|
||||
};
|
||||
|
||||
const bem = bemGenerator('module-conversation-details-panel-section');
|
||||
const bem = bemGenerator('ConversationDetails-panel-section');
|
||||
const borderlessClass = bem('root', 'borderless');
|
||||
|
||||
export const PanelSection: React.ComponentType<Props> = ({
|
||||
|
|
|
@ -11,7 +11,7 @@ import { Avatar } from '../../Avatar';
|
|||
import { ConfirmationDialog } from '../../ConfirmationDialog';
|
||||
import { PanelSection } from './PanelSection';
|
||||
import { PanelRow } from './PanelRow';
|
||||
import { ConversationDetailsIcon } from './ConversationDetailsIcon';
|
||||
import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon';
|
||||
|
||||
export type PropsType = {
|
||||
readonly conversation?: ConversationType;
|
||||
|
@ -73,12 +73,11 @@ export const PendingInvites: React.ComponentType<PropsType> = ({
|
|||
|
||||
return (
|
||||
<div className="conversation-details-panel">
|
||||
<div className="module-conversation-details__tabs">
|
||||
<div className="ConversationDetails__tabs">
|
||||
<div
|
||||
className={classNames({
|
||||
'module-conversation-details__tab': true,
|
||||
'module-conversation-details__tab--selected':
|
||||
selectedTab === Tab.Requests,
|
||||
ConversationDetails__tab: true,
|
||||
'ConversationDetails__tab--selected': selectedTab === Tab.Requests,
|
||||
})}
|
||||
onClick={() => {
|
||||
setSelectedTab(Tab.Requests);
|
||||
|
@ -98,9 +97,8 @@ export const PendingInvites: React.ComponentType<PropsType> = ({
|
|||
|
||||
<div
|
||||
className={classNames({
|
||||
'module-conversation-details__tab': true,
|
||||
'module-conversation-details__tab--selected':
|
||||
selectedTab === Tab.Pending,
|
||||
ConversationDetails__tab: true,
|
||||
'ConversationDetails__tab--selected': selectedTab === Tab.Pending,
|
||||
})}
|
||||
onClick={() => {
|
||||
setSelectedTab(Tab.Pending);
|
||||
|
@ -323,7 +321,7 @@ function MembersPendingAdminApproval({
|
|||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="module-button__small module-conversation-details__action-button"
|
||||
className="module-button__small ConversationDetails__action-button"
|
||||
onClick={() => {
|
||||
setStagedMemberships([
|
||||
{
|
||||
|
@ -337,7 +335,7 @@ function MembersPendingAdminApproval({
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="module-button__small module-conversation-details__action-button"
|
||||
className="module-button__small ConversationDetails__action-button"
|
||||
onClick={() => {
|
||||
setStagedMemberships([
|
||||
{
|
||||
|
@ -354,7 +352,7 @@ function MembersPendingAdminApproval({
|
|||
}
|
||||
/>
|
||||
))}
|
||||
<div className="module-conversation-details__pending--info">
|
||||
<div className="ConversationDetails__pending--info">
|
||||
{i18n('PendingRequests--info', [conversation.title])}
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
@ -414,7 +412,7 @@ function MembersPendingProfileKey({
|
|||
conversation.areWeAdmin ? (
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('PendingInvites--revoke-for-label')}
|
||||
icon="trash"
|
||||
icon={IconType.trash}
|
||||
onClick={() => {
|
||||
setStagedMemberships([
|
||||
{
|
||||
|
@ -451,7 +449,7 @@ function MembersPendingProfileKey({
|
|||
conversation.areWeAdmin ? (
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('PendingInvites--revoke-for-label')}
|
||||
icon="trash"
|
||||
icon={IconType.trash}
|
||||
onClick={() => {
|
||||
setStagedMemberships(
|
||||
pendingMemberships.map(membership => ({
|
||||
|
@ -467,7 +465,7 @@ function MembersPendingProfileKey({
|
|||
))}
|
||||
</PanelSection>
|
||||
)}
|
||||
<div className="module-conversation-details__pending--info">
|
||||
<div className="ConversationDetails__pending--info">
|
||||
{i18n('PendingInvites--info')}
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue