// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { useState, ReactNode } from 'react'; import { ConversationType } from '../../../state/ducks/conversations'; import { assert } from '../../../util/assert'; import { getMutedUntilText } from '../../../util/getMutedUntilText'; import { LocalizerType } from '../../../types/Util'; import { MediaItemType } from '../../../types/MediaItem'; import { missingCaseError } from '../../../util/missingCaseError'; import { DisappearingTimerSelect } from '../../DisappearingTimerSelect'; import { PanelRow } from './PanelRow'; import { PanelSection } from './PanelSection'; 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'; import { EditConversationAttributesModal } from './EditConversationAttributesModal'; import { RequestState } from './util'; import { getCustomColorStyle } from '../../../util/getCustomColorStyle'; import { ConfirmationDialog } from '../../ConfirmationDialog'; import { AvatarDataType, DeleteAvatarFromDiskActionType, ReplaceAvatarActionType, SaveAvatarToDiskActionType, } from '../../../types/Avatar'; enum ModalState { NothingOpen, EditingGroupDescription, EditingGroupTitle, AddingGroupMembers, } export type StateProps = { addMembers: (conversationIds: ReadonlyArray) => Promise; canEditGroupInfo: boolean; candidateContactsToAdd: Array; conversation?: ConversationType; hasGroupLink: boolean; i18n: LocalizerType; isAdmin: boolean; loadRecentMediaItems: (limit: number) => void; memberships: Array; pendingApprovalMemberships: ReadonlyArray; pendingMemberships: ReadonlyArray; setDisappearingMessages: (seconds: number) => void; showAllMedia: () => void; showContactModal: (conversationId: string) => void; showGroupChatColorEditor: () => void; showGroupLinkManagement: () => void; showGroupV2Permissions: () => void; showPendingInvites: () => void; showLightboxForMedia: ( selectedMediaItem: MediaItemType, media: Array ) => void; showConversationNotificationsSettings: () => void; updateGroupAttributes: ( _: Readonly<{ avatar?: undefined | ArrayBuffer; description?: string; title?: string; }> ) => Promise; onBlock: () => void; onLeave: () => void; userAvatarData: Array; }; type ActionProps = { deleteAvatarFromDisk: DeleteAvatarFromDiskActionType; replaceAvatar: ReplaceAvatarActionType; saveAvatarToDisk: SaveAvatarToDiskActionType; }; export type Props = StateProps & ActionProps; export const ConversationDetails: React.ComponentType = ({ addMembers, canEditGroupInfo, candidateContactsToAdd, conversation, hasGroupLink, i18n, isAdmin, loadRecentMediaItems, memberships, pendingApprovalMemberships, pendingMemberships, setDisappearingMessages, showAllMedia, showContactModal, showGroupChatColorEditor, showGroupLinkManagement, showGroupV2Permissions, showPendingInvites, showLightboxForMedia, showConversationNotificationsSettings, updateGroupAttributes, onBlock, onLeave, deleteAvatarFromDisk, replaceAvatar, saveAvatarToDisk, userAvatarData, }) => { const [modalState, setModalState] = useState( ModalState.NothingOpen ); const [ editGroupAttributesRequestState, setEditGroupAttributesRequestState, ] = useState(RequestState.Inactive); const [ addGroupMembersRequestState, setAddGroupMembersRequestState, ] = useState(RequestState.Inactive); 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; let modalNode: ReactNode; switch (modalState) { case ModalState.NothingOpen: modalNode = undefined; break; case ModalState.EditingGroupDescription: case ModalState.EditingGroupTitle: modalNode = ( ) => { 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} deleteAvatarFromDisk={deleteAvatarFromDisk} replaceAvatar={replaceAvatar} saveAvatarToDisk={saveAvatarToDisk} userAvatarData={userAvatarData} /> ); break; case ModalState.AddingGroupMembers: modalNode = ( { 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)) } groupTitle={conversation.title} i18n={i18n} makeRequest={async conversationIds => { setAddGroupMembersRequestState(RequestState.Active); try { await addMembers(conversationIds); setModalState(ModalState.NothingOpen); setAddGroupMembersRequestState(RequestState.Inactive); } catch (err) { if (err.code === 'E_NO_CAPABILITY') { setMembersMissingCapability(true); setAddGroupMembersRequestState(RequestState.InactiveWithError); } else { setAddGroupMembersRequestState(RequestState.InactiveWithError); } } }} onClose={() => { setModalState(ModalState.NothingOpen); setEditGroupAttributesRequestState(RequestState.Inactive); }} requestState={addGroupMembersRequestState} /> ); break; default: throw missingCaseError(modalState); } return (
{membersMissingCapability && ( setMembersMissingCapability(false)} > {i18n('GroupV2--add--missing-capability')} )} { setModalState( isGroupTitle ? ModalState.EditingGroupTitle : ModalState.EditingGroupDescription ); }} /> {canEditGroupInfo ? ( } info={i18n('ConversationDetails--disappearing-messages-info')} label={i18n('ConversationDetails--disappearing-messages-label')} right={ } /> ) : null} } label={i18n('showChatColorEditor')} onClick={showGroupChatColorEditor} right={
} /> } label={i18n('ConversationDetails--notifications')} onClick={showConversationNotificationsSettings} right={ conversation.muteExpiresAt ? getMutedUntilText(conversation.muteExpiresAt, i18n) : undefined } /> { setModalState(ModalState.AddingGroupMembers); }} /> {isAdmin || hasGroupLink ? ( } label={i18n('ConversationDetails--group-link')} onClick={showGroupLinkManagement} right={hasGroupLink ? i18n('on') : i18n('off')} /> ) : null} } label={i18n('ConversationDetails--requests-and-invites')} onClick={showPendingInvites} right={invitesCount} /> {isAdmin ? ( } label={i18n('permissions')} onClick={showGroupV2Permissions} /> ) : null} {modalNode}
); };