2021-03-09 19:16:56 +00:00
|
|
|
// Copyright 2021 Signal Messenger, LLC
|
2021-01-29 21:19:24 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2021-03-11 21:29:31 +00:00
|
|
|
import React, { useState, ReactNode } from 'react';
|
2021-01-29 21:19:24 +00:00
|
|
|
|
|
|
|
import { ConversationType } from '../../../state/ducks/conversations';
|
2021-03-11 21:29:31 +00:00
|
|
|
import { assert } from '../../../util/assert';
|
2021-05-03 23:24:40 +00:00
|
|
|
import * as expirationTimer from '../../../util/expirationTimer';
|
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
import { LocalizerType } from '../../../types/Util';
|
|
|
|
import { MediaItemType } from '../../LightboxGallery';
|
2021-03-11 21:29:31 +00:00
|
|
|
import { missingCaseError } from '../../../util/missingCaseError';
|
2021-01-29 21:19:24 +00:00
|
|
|
|
|
|
|
import { PanelRow } from './PanelRow';
|
|
|
|
import { PanelSection } from './PanelSection';
|
2021-03-11 21:29:31 +00:00
|
|
|
import { AddGroupMembersModal } from './AddGroupMembersModal';
|
2021-01-29 21:19:24 +00:00
|
|
|
import { ConversationDetailsActions } from './ConversationDetailsActions';
|
|
|
|
import { ConversationDetailsHeader } from './ConversationDetailsHeader';
|
|
|
|
import { ConversationDetailsIcon } from './ConversationDetailsIcon';
|
|
|
|
import { ConversationDetailsMediaList } from './ConversationDetailsMediaList';
|
2021-04-29 18:32:38 +00:00
|
|
|
import {
|
|
|
|
ConversationDetailsMembershipList,
|
|
|
|
GroupV2Membership,
|
|
|
|
} from './ConversationDetailsMembershipList';
|
2021-03-11 21:29:31 +00:00
|
|
|
import { EditConversationAttributesModal } from './EditConversationAttributesModal';
|
|
|
|
import { RequestState } from './util';
|
|
|
|
|
|
|
|
enum ModalState {
|
|
|
|
NothingOpen,
|
|
|
|
EditingGroupAttributes,
|
|
|
|
AddingGroupMembers,
|
|
|
|
}
|
2021-01-29 21:19:24 +00:00
|
|
|
|
|
|
|
export type StateProps = {
|
2021-03-11 21:29:31 +00:00
|
|
|
addMembers: (conversationIds: ReadonlyArray<string>) => Promise<void>;
|
2021-01-29 21:19:24 +00:00
|
|
|
canEditGroupInfo: boolean;
|
2021-03-11 21:29:31 +00:00
|
|
|
candidateContactsToAdd: Array<ConversationType>;
|
2021-01-29 21:19:24 +00:00
|
|
|
conversation?: ConversationType;
|
|
|
|
hasGroupLink: boolean;
|
|
|
|
i18n: LocalizerType;
|
|
|
|
isAdmin: boolean;
|
|
|
|
loadRecentMediaItems: (limit: number) => void;
|
2021-04-29 18:32:38 +00:00
|
|
|
memberships: Array<GroupV2Membership>;
|
2021-01-29 21:19:24 +00:00
|
|
|
setDisappearingMessages: (seconds: number) => void;
|
|
|
|
showAllMedia: () => void;
|
|
|
|
showContactModal: (conversationId: string) => void;
|
|
|
|
showGroupLinkManagement: () => void;
|
|
|
|
showGroupV2Permissions: () => void;
|
|
|
|
showPendingInvites: () => void;
|
|
|
|
showLightboxForMedia: (
|
|
|
|
selectedMediaItem: MediaItemType,
|
|
|
|
media: Array<MediaItemType>
|
|
|
|
) => void;
|
2021-03-09 19:16:56 +00:00
|
|
|
updateGroupAttributes: (
|
|
|
|
_: Readonly<{
|
|
|
|
avatar?: undefined | ArrayBuffer;
|
|
|
|
title?: string;
|
|
|
|
}>
|
2021-03-12 23:31:47 +00:00
|
|
|
) => Promise<void>;
|
2021-04-28 20:27:16 +00:00
|
|
|
onBlock: () => void;
|
|
|
|
onLeave: () => void;
|
2021-01-29 21:19:24 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
export type Props = StateProps;
|
|
|
|
|
2021-05-11 17:05:02 +00:00
|
|
|
const expirationTimerDefaultSet = new Set<number>(
|
|
|
|
expirationTimer.DEFAULT_DURATIONS_IN_SECONDS
|
|
|
|
);
|
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
export const ConversationDetails: React.ComponentType<Props> = ({
|
2021-03-11 21:29:31 +00:00
|
|
|
addMembers,
|
2021-01-29 21:19:24 +00:00
|
|
|
canEditGroupInfo,
|
2021-03-11 21:29:31 +00:00
|
|
|
candidateContactsToAdd,
|
2021-01-29 21:19:24 +00:00
|
|
|
conversation,
|
|
|
|
hasGroupLink,
|
|
|
|
i18n,
|
|
|
|
isAdmin,
|
|
|
|
loadRecentMediaItems,
|
2021-04-29 18:32:38 +00:00
|
|
|
memberships,
|
2021-01-29 21:19:24 +00:00
|
|
|
setDisappearingMessages,
|
|
|
|
showAllMedia,
|
|
|
|
showContactModal,
|
|
|
|
showGroupLinkManagement,
|
|
|
|
showGroupV2Permissions,
|
|
|
|
showPendingInvites,
|
|
|
|
showLightboxForMedia,
|
2021-03-09 19:16:56 +00:00
|
|
|
updateGroupAttributes,
|
2021-04-28 20:27:16 +00:00
|
|
|
onBlock,
|
|
|
|
onLeave,
|
2021-01-29 21:19:24 +00:00
|
|
|
}) => {
|
2021-03-11 21:29:31 +00:00
|
|
|
const [modalState, setModalState] = useState<ModalState>(
|
|
|
|
ModalState.NothingOpen
|
2021-03-09 19:16:56 +00:00
|
|
|
);
|
|
|
|
const [
|
|
|
|
editGroupAttributesRequestState,
|
|
|
|
setEditGroupAttributesRequestState,
|
2021-03-11 21:29:31 +00:00
|
|
|
] = useState<RequestState>(RequestState.Inactive);
|
|
|
|
const [
|
|
|
|
addGroupMembersRequestState,
|
|
|
|
setAddGroupMembersRequestState,
|
|
|
|
] = useState<RequestState>(RequestState.Inactive);
|
2021-03-09 19:16:56 +00:00
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
const updateExpireTimer = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
|
|
setDisappearingMessages(parseInt(event.target.value, 10));
|
|
|
|
};
|
|
|
|
|
|
|
|
if (conversation === undefined) {
|
|
|
|
throw new Error('ConversationDetails rendered without a conversation');
|
|
|
|
}
|
|
|
|
|
|
|
|
const pendingMemberships = conversation.pendingMemberships || [];
|
|
|
|
const pendingApprovalMemberships =
|
|
|
|
conversation.pendingApprovalMemberships || [];
|
|
|
|
const invitesCount =
|
|
|
|
pendingMemberships.length + pendingApprovalMemberships.length;
|
|
|
|
|
2021-04-05 17:44:13 +00:00
|
|
|
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.EditingGroupAttributes:
|
|
|
|
modalNode = (
|
|
|
|
<EditConversationAttributesModal
|
|
|
|
avatarPath={conversation.avatarPath}
|
|
|
|
i18n={i18n}
|
|
|
|
makeRequest={async (
|
|
|
|
options: Readonly<{
|
|
|
|
avatar?: undefined | ArrayBuffer;
|
|
|
|
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}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
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={
|
2021-04-05 17:44:13 +00:00
|
|
|
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) {
|
|
|
|
setAddGroupMembersRequestState(RequestState.InactiveWithError);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
onClose={() => {
|
|
|
|
setModalState(ModalState.NothingOpen);
|
|
|
|
setEditGroupAttributesRequestState(RequestState.Inactive);
|
|
|
|
}}
|
|
|
|
requestState={addGroupMembersRequestState}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw missingCaseError(modalState);
|
|
|
|
}
|
|
|
|
|
2021-05-11 17:05:02 +00:00
|
|
|
const expireTimer = conversation.expireTimer || 0;
|
|
|
|
|
|
|
|
let expirationTimerDurations = expirationTimer.DEFAULT_DURATIONS_IN_SECONDS;
|
|
|
|
if (!expirationTimerDefaultSet.has(expireTimer)) {
|
|
|
|
expirationTimerDurations = [...expirationTimerDurations, expireTimer];
|
|
|
|
}
|
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
return (
|
|
|
|
<div className="conversation-details-panel">
|
2021-03-09 19:16:56 +00:00
|
|
|
<ConversationDetailsHeader
|
|
|
|
canEdit={canEditGroupInfo}
|
|
|
|
conversation={conversation}
|
|
|
|
i18n={i18n}
|
|
|
|
startEditing={() => {
|
2021-03-11 21:29:31 +00:00
|
|
|
setModalState(ModalState.EditingGroupAttributes);
|
2021-03-09 19:16:56 +00:00
|
|
|
}}
|
|
|
|
/>
|
2021-01-29 21:19:24 +00:00
|
|
|
|
|
|
|
{canEditGroupInfo ? (
|
|
|
|
<PanelSection>
|
|
|
|
<PanelRow
|
|
|
|
icon={
|
|
|
|
<ConversationDetailsIcon
|
|
|
|
ariaLabel={i18n(
|
|
|
|
'ConversationDetails--disappearing-messages-label'
|
|
|
|
)}
|
|
|
|
icon="timer"
|
|
|
|
/>
|
|
|
|
}
|
|
|
|
info={i18n('ConversationDetails--disappearing-messages-info')}
|
|
|
|
label={i18n('ConversationDetails--disappearing-messages-label')}
|
|
|
|
right={
|
|
|
|
<div className="module-conversation-details-select">
|
2021-05-11 17:05:02 +00:00
|
|
|
<select onChange={updateExpireTimer} value={expireTimer}>
|
|
|
|
{expirationTimerDurations.map((seconds: number) => {
|
|
|
|
const label = expirationTimer.format(i18n, seconds);
|
|
|
|
return (
|
|
|
|
<option value={seconds} key={seconds} aria-label={label}>
|
|
|
|
{label}
|
|
|
|
</option>
|
|
|
|
);
|
|
|
|
})}
|
2021-01-29 21:19:24 +00:00
|
|
|
</select>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</PanelSection>
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
<ConversationDetailsMembershipList
|
2021-03-11 21:29:31 +00:00
|
|
|
canAddNewMembers={canEditGroupInfo}
|
2021-01-29 21:19:24 +00:00
|
|
|
i18n={i18n}
|
2021-04-05 17:44:13 +00:00
|
|
|
memberships={memberships}
|
2021-03-11 21:29:31 +00:00
|
|
|
showContactModal={showContactModal}
|
|
|
|
startAddingNewMembers={() => {
|
|
|
|
setModalState(ModalState.AddingGroupMembers);
|
|
|
|
}}
|
2021-01-29 21:19:24 +00:00
|
|
|
/>
|
|
|
|
|
|
|
|
<PanelSection>
|
2021-03-02 16:27:11 +00:00
|
|
|
{isAdmin || hasGroupLink ? (
|
2021-01-29 21:19:24 +00:00
|
|
|
<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}
|
2021-04-05 17:44:13 +00:00
|
|
|
cannotLeaveBecauseYouAreLastAdmin={cannotLeaveBecauseYouAreLastAdmin}
|
2021-01-29 21:19:24 +00:00
|
|
|
conversationTitle={conversation.title}
|
2021-04-28 20:27:16 +00:00
|
|
|
onLeave={onLeave}
|
|
|
|
onBlock={onBlock}
|
2021-01-29 21:19:24 +00:00
|
|
|
/>
|
2021-03-09 19:16:56 +00:00
|
|
|
|
2021-03-11 21:29:31 +00:00
|
|
|
{modalNode}
|
2021-01-29 21:19:24 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|