signal-desktop/ts/components/conversation/conversation-details/ConversationDetails.tsx

399 lines
12 KiB
TypeScript
Raw Normal View History

// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
2021-03-11 21:29:31 +00:00
import React, { useState, ReactNode } from 'react';
import { ConversationType } from '../../../state/ducks/conversations';
2021-03-11 21:29:31 +00:00
import { assert } from '../../../util/assert';
2021-08-05 12:35:33 +00:00
import { getMutedUntilText } from '../../../util/getMutedUntilText';
import { LocalizerType } from '../../../types/Util';
import { MediaItemType } from '../../LightboxGallery';
2021-03-11 21:29:31 +00:00
import { missingCaseError } from '../../../util/missingCaseError';
import { DisappearingTimerSelect } from '../../DisappearingTimerSelect';
2021-06-01 20:45:43 +00:00
import { PanelRow } from './PanelRow';
import { PanelSection } from './PanelSection';
2021-03-11 21:29:31 +00:00
import { AddGroupMembersModal } from './AddGroupMembersModal';
import { ConversationDetailsActions } from './ConversationDetailsActions';
import { ConversationDetailsHeader } from './ConversationDetailsHeader';
import { ConversationDetailsIcon } from './ConversationDetailsIcon';
import { ConversationDetailsMediaList } from './ConversationDetailsMediaList';
import {
ConversationDetailsMembershipList,
GroupV2Membership,
} from './ConversationDetailsMembershipList';
import {
GroupV2PendingMembership,
GroupV2RequestingMembership,
} from './PendingInvites';
2021-03-11 21:29:31 +00:00
import { EditConversationAttributesModal } from './EditConversationAttributesModal';
import { RequestState } from './util';
2021-05-28 16:15:17 +00:00
import { getCustomColorStyle } from '../../../util/getCustomColorStyle';
2021-07-20 20:18:35 +00:00
import { ConfirmationDialog } from '../../ConfirmationDialog';
2021-08-06 00:17:05 +00:00
import {
AvatarDataType,
DeleteAvatarFromDiskActionType,
ReplaceAvatarActionType,
SaveAvatarToDiskActionType,
} from '../../../types/Avatar';
2021-03-11 21:29:31 +00:00
enum ModalState {
NothingOpen,
EditingGroupDescription,
EditingGroupTitle,
2021-03-11 21:29:31 +00:00
AddingGroupMembers,
}
export type StateProps = {
2021-03-11 21:29:31 +00:00
addMembers: (conversationIds: ReadonlyArray<string>) => Promise<void>;
canEditGroupInfo: boolean;
2021-03-11 21:29:31 +00:00
candidateContactsToAdd: Array<ConversationType>;
conversation?: ConversationType;
hasGroupLink: boolean;
i18n: LocalizerType;
isAdmin: boolean;
loadRecentMediaItems: (limit: number) => void;
memberships: Array<GroupV2Membership>;
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
setDisappearingMessages: (seconds: number) => void;
showAllMedia: () => void;
showContactModal: (conversationId: string) => void;
2021-05-28 16:15:17 +00:00
showGroupChatColorEditor: () => void;
showGroupLinkManagement: () => void;
showGroupV2Permissions: () => void;
showPendingInvites: () => void;
showLightboxForMedia: (
selectedMediaItem: MediaItemType,
media: Array<MediaItemType>
) => void;
2021-08-05 12:35:33 +00:00
showConversationNotificationsSettings: () => void;
updateGroupAttributes: (
_: Readonly<{
avatar?: undefined | ArrayBuffer;
2021-06-02 00:24:28 +00:00
description?: string;
title?: string;
}>
) => Promise<void>;
onBlock: () => void;
onLeave: () => void;
2021-08-06 00:17:05 +00:00
userAvatarData: Array<AvatarDataType>;
};
type ActionProps = {
deleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
replaceAvatar: ReplaceAvatarActionType;
saveAvatarToDisk: SaveAvatarToDiskActionType;
};
2021-08-06 00:17:05 +00:00
export type Props = StateProps & ActionProps;
export const ConversationDetails: React.ComponentType<Props> = ({
2021-03-11 21:29:31 +00:00
addMembers,
canEditGroupInfo,
2021-03-11 21:29:31 +00:00
candidateContactsToAdd,
conversation,
hasGroupLink,
i18n,
isAdmin,
loadRecentMediaItems,
memberships,
pendingApprovalMemberships,
pendingMemberships,
setDisappearingMessages,
showAllMedia,
showContactModal,
2021-05-28 16:15:17 +00:00
showGroupChatColorEditor,
showGroupLinkManagement,
showGroupV2Permissions,
showPendingInvites,
showLightboxForMedia,
2021-08-05 12:35:33 +00:00
showConversationNotificationsSettings,
updateGroupAttributes,
onBlock,
onLeave,
2021-08-06 00:17:05 +00:00
deleteAvatarFromDisk,
replaceAvatar,
saveAvatarToDisk,
userAvatarData,
}) => {
2021-03-11 21:29:31 +00:00
const [modalState, setModalState] = useState<ModalState>(
ModalState.NothingOpen
);
const [
editGroupAttributesRequestState,
setEditGroupAttributesRequestState,
2021-03-11 21:29:31 +00:00
] = useState<RequestState>(RequestState.Inactive);
const [
addGroupMembersRequestState,
setAddGroupMembersRequestState,
] = useState<RequestState>(RequestState.Inactive);
2021-07-20 20:18:35 +00:00
const [membersMissingCapability, setMembersMissingCapability] = useState(
false
);
if (conversation === undefined) {
throw new Error('ConversationDetails rendered without a conversation');
}
const invitesCount =
pendingMemberships.length + pendingApprovalMemberships.length;
const otherMemberships = memberships.filter(({ member }) => !member.isMe);
const isJustMe = otherMemberships.length === 0;
const isAnyoneElseAnAdmin = otherMemberships.some(
membership => membership.isAdmin
);
const cannotLeaveBecauseYouAreLastAdmin =
isAdmin && !isJustMe && !isAnyoneElseAnAdmin;
2021-03-11 21:29:31 +00:00
let modalNode: ReactNode;
switch (modalState) {
case ModalState.NothingOpen:
modalNode = undefined;
break;
case ModalState.EditingGroupDescription:
case ModalState.EditingGroupTitle:
2021-03-11 21:29:31 +00:00
modalNode = (
<EditConversationAttributesModal
2021-08-06 00:17:05 +00:00
avatarColor={conversation.color}
2021-03-11 21:29:31 +00:00
avatarPath={conversation.avatarPath}
2021-08-06 00:17:05 +00:00
conversationId={conversation.id}
2021-06-02 00:24:28 +00:00
groupDescription={conversation.groupDescription}
2021-03-11 21:29:31 +00:00
i18n={i18n}
initiallyFocusDescription={
modalState === ModalState.EditingGroupDescription
}
2021-03-11 21:29:31 +00:00
makeRequest={async (
options: Readonly<{
avatar?: undefined | ArrayBuffer;
2021-06-02 00:24:28 +00:00
description?: string;
2021-03-11 21:29:31 +00:00
title?: string;
}>
) => {
setEditGroupAttributesRequestState(RequestState.Active);
try {
await updateGroupAttributes(options);
setModalState(ModalState.NothingOpen);
setEditGroupAttributesRequestState(RequestState.Inactive);
} catch (err) {
setEditGroupAttributesRequestState(
RequestState.InactiveWithError
);
}
}}
onClose={() => {
setModalState(ModalState.NothingOpen);
setEditGroupAttributesRequestState(RequestState.Inactive);
}}
requestState={editGroupAttributesRequestState}
title={conversation.title}
2021-08-06 00:17:05 +00:00
deleteAvatarFromDisk={deleteAvatarFromDisk}
replaceAvatar={replaceAvatar}
saveAvatarToDisk={saveAvatarToDisk}
userAvatarData={userAvatarData}
2021-03-11 21:29:31 +00:00
/>
);
break;
case ModalState.AddingGroupMembers:
modalNode = (
<AddGroupMembersModal
candidateContacts={candidateContactsToAdd}
clearRequestError={() => {
setAddGroupMembersRequestState(oldRequestState => {
assert(
oldRequestState !== RequestState.Active,
'Should not be clearing an active request state'
);
return RequestState.Inactive;
});
}}
conversationIdsAlreadyInGroup={
new Set(memberships.map(membership => membership.member.id))
2021-03-11 21:29:31 +00:00
}
groupTitle={conversation.title}
i18n={i18n}
makeRequest={async conversationIds => {
setAddGroupMembersRequestState(RequestState.Active);
try {
await addMembers(conversationIds);
setModalState(ModalState.NothingOpen);
setAddGroupMembersRequestState(RequestState.Inactive);
} catch (err) {
2021-07-20 20:18:35 +00:00
if (err.code === 'E_NO_CAPABILITY') {
setMembersMissingCapability(true);
setAddGroupMembersRequestState(RequestState.InactiveWithError);
} else {
setAddGroupMembersRequestState(RequestState.InactiveWithError);
}
2021-03-11 21:29:31 +00:00
}
}}
onClose={() => {
setModalState(ModalState.NothingOpen);
setEditGroupAttributesRequestState(RequestState.Inactive);
}}
requestState={addGroupMembersRequestState}
/>
);
break;
default:
throw missingCaseError(modalState);
}
return (
<div className="conversation-details-panel">
2021-07-20 20:18:35 +00:00
{membersMissingCapability && (
<ConfirmationDialog
cancelText={i18n('Confirmation--confirm')}
i18n={i18n}
onClose={() => setMembersMissingCapability(false)}
>
{i18n('GroupV2--add--missing-capability')}
</ConfirmationDialog>
)}
<ConversationDetailsHeader
canEdit={canEditGroupInfo}
conversation={conversation}
i18n={i18n}
memberships={memberships}
startEditing={(isGroupTitle: boolean) => {
setModalState(
isGroupTitle
? ModalState.EditingGroupTitle
: ModalState.EditingGroupDescription
);
}}
/>
2021-05-28 16:15:17 +00:00
<PanelSection>
{canEditGroupInfo ? (
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n(
'ConversationDetails--disappearing-messages-label'
)}
icon="timer"
/>
}
info={i18n('ConversationDetails--disappearing-messages-info')}
label={i18n('ConversationDetails--disappearing-messages-label')}
right={
<DisappearingTimerSelect
i18n={i18n}
value={conversation.expireTimer || 0}
onChange={setDisappearingMessages}
2021-06-01 20:45:43 +00:00
/>
}
/>
2021-05-28 16:15:17 +00:00
) : null}
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('showChatColorEditor')}
icon="color"
/>
}
label={i18n('showChatColorEditor')}
onClick={showGroupChatColorEditor}
right={
<div
className={`module-conversation-details__chat-color module-conversation-details__chat-color--${conversation.conversationColor}`}
style={{
...getCustomColorStyle(conversation.customColor),
}}
/>
}
/>
2021-08-05 12:35:33 +00:00
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('ConversationDetails--notifications')}
icon="notifications"
/>
}
label={i18n('ConversationDetails--notifications')}
onClick={showConversationNotificationsSettings}
right={
conversation.muteExpiresAt
? getMutedUntilText(conversation.muteExpiresAt, i18n)
: undefined
}
/>
2021-05-28 16:15:17 +00:00
</PanelSection>
<ConversationDetailsMembershipList
2021-03-11 21:29:31 +00:00
canAddNewMembers={canEditGroupInfo}
i18n={i18n}
memberships={memberships}
2021-03-11 21:29:31 +00:00
showContactModal={showContactModal}
startAddingNewMembers={() => {
setModalState(ModalState.AddingGroupMembers);
}}
/>
<PanelSection>
2021-03-02 16:27:11 +00:00
{isAdmin || hasGroupLink ? (
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('ConversationDetails--group-link')}
icon="link"
/>
}
label={i18n('ConversationDetails--group-link')}
onClick={showGroupLinkManagement}
right={hasGroupLink ? i18n('on') : i18n('off')}
/>
) : null}
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('ConversationDetails--requests-and-invites')}
icon="invites"
/>
}
label={i18n('ConversationDetails--requests-and-invites')}
onClick={showPendingInvites}
right={invitesCount}
/>
{isAdmin ? (
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('permissions')}
icon="lock"
/>
}
label={i18n('permissions')}
onClick={showGroupV2Permissions}
/>
) : null}
</PanelSection>
<ConversationDetailsMediaList
conversation={conversation}
i18n={i18n}
loadRecentMediaItems={loadRecentMediaItems}
showAllMedia={showAllMedia}
showLightboxForMedia={showLightboxForMedia}
/>
<ConversationDetailsActions
i18n={i18n}
cannotLeaveBecauseYouAreLastAdmin={cannotLeaveBecauseYouAreLastAdmin}
conversationTitle={conversation.title}
onLeave={onLeave}
onBlock={onBlock}
/>
2021-03-11 21:29:31 +00:00
{modalNode}
</div>
);
};