// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; import classNames from 'classnames'; import _ from 'lodash'; import type { ConversationType } from '../../../state/ducks/conversations'; import type { LocalizerType } from '../../../types/Util'; import type { UUIDStringType } from '../../../types/UUID'; import { Avatar } from '../../Avatar'; import { ConfirmationDialog } from '../../ConfirmationDialog'; import { PanelSection } from './PanelSection'; import { PanelRow } from './PanelRow'; import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon'; export type PropsType = { readonly conversation?: ConversationType; readonly i18n: LocalizerType; readonly ourUuid: UUIDStringType; readonly pendingApprovalMemberships: ReadonlyArray; readonly pendingMemberships: ReadonlyArray; readonly approvePendingMembership: (conversationId: string) => void; readonly revokePendingMemberships: (conversationIds: Array) => void; }; export type GroupV2PendingMembership = { metadata: { addedByUserId?: UUIDStringType; }; member: ConversationType; }; export type GroupV2RequestingMembership = { member: ConversationType; }; enum Tab { Requests = 'Requests', Pending = 'Pending', } enum StageType { APPROVE_REQUEST = 'APPROVE_REQUEST', DENY_REQUEST = 'DENY_REQUEST', REVOKE_INVITE = 'REVOKE_INVITE', } type StagedMembershipType = { type: StageType; membership: GroupV2PendingMembership | GroupV2RequestingMembership; }; export const PendingInvites: React.ComponentType = ({ approvePendingMembership, conversation, i18n, ourUuid, pendingMemberships, pendingApprovalMemberships, revokePendingMemberships, }) => { if (!conversation || !ourUuid) { throw new Error( 'PendingInvites rendered without a conversation or ourUuid' ); } const [selectedTab, setSelectedTab] = React.useState(Tab.Requests); const [stagedMemberships, setStagedMemberships] = React.useState | null>(null); return (
{ setSelectedTab(Tab.Requests); }} onKeyUp={(e: React.KeyboardEvent) => { if (e.target === e.currentTarget && e.keyCode === 13) { setSelectedTab(Tab.Requests); } }} role="tab" tabIndex={0} > {i18n('PendingInvites--tab-requests', { count: String(pendingApprovalMemberships.length), })}
{ setSelectedTab(Tab.Pending); }} onKeyUp={(e: React.KeyboardEvent) => { if (e.target === e.currentTarget && e.keyCode === 13) { setSelectedTab(Tab.Pending); } }} role="tab" tabIndex={0} > {i18n('PendingInvites--tab-invites', { count: String(pendingMemberships.length), })}
{selectedTab === Tab.Requests ? ( ) : null} {selectedTab === Tab.Pending ? ( ) : null} {stagedMemberships && stagedMemberships.length && ( setStagedMemberships(null)} ourUuid={ourUuid} revokePendingMemberships={revokePendingMemberships} stagedMemberships={stagedMemberships} /> )}
); }; function MembershipActionConfirmation({ approvePendingMembership, i18n, members, onClose, ourUuid, revokePendingMemberships, stagedMemberships, }: { approvePendingMembership: (conversationId: string) => void; i18n: LocalizerType; members: Array; onClose: () => void; ourUuid: string; revokePendingMemberships: (conversationIds: Array) => void; stagedMemberships: Array; }) { const revokeStagedMemberships = () => { if (!stagedMemberships) { return; } revokePendingMemberships( stagedMemberships.map(({ membership }) => membership.member.id) ); }; const approveStagedMembership = () => { if (!stagedMemberships) { return; } approvePendingMembership(stagedMemberships[0].membership.member.id); }; const membershipType = stagedMemberships[0].type; const modalAction = membershipType === StageType.APPROVE_REQUEST ? approveStagedMembership : revokeStagedMemberships; let modalActionText = i18n('PendingInvites--revoke'); if (membershipType === StageType.APPROVE_REQUEST) { modalActionText = i18n('PendingRequests--approve'); } else if (membershipType === StageType.DENY_REQUEST) { modalActionText = i18n('PendingRequests--deny'); } else if (membershipType === StageType.REVOKE_INVITE) { modalActionText = i18n('PendingInvites--revoke'); } return ( {getConfirmationMessage({ i18n, members, ourUuid, stagedMemberships, })} ); } function getConfirmationMessage({ i18n, members, ourUuid, stagedMemberships, }: Readonly<{ i18n: LocalizerType; members: ReadonlyArray; ourUuid: string; stagedMemberships: ReadonlyArray; }>): string { if (!stagedMemberships || !stagedMemberships.length) { return ''; } const membershipType = stagedMemberships[0].type; const firstMembership = stagedMemberships[0].membership; // Requesting a membership since they weren't added by anyone if (membershipType === StageType.DENY_REQUEST) { return i18n('PendingRequests--deny-for', { name: firstMembership.member.title, }); } if (membershipType === StageType.APPROVE_REQUEST) { return i18n('PendingRequests--approve-for', { name: firstMembership.member.title, }); } if (membershipType !== StageType.REVOKE_INVITE) { throw new Error('getConfirmationMessage: Invalid staging type'); } const firstPendingMembership = firstMembership as GroupV2PendingMembership; // Pending invite const invitedByUs = firstPendingMembership.metadata.addedByUserId === ourUuid; if (invitedByUs) { return i18n('PendingInvites--revoke-for', { name: firstPendingMembership.member.title, }); } const inviter = members.find( ({ id }) => id === firstPendingMembership.metadata.addedByUserId ); if (inviter === undefined) { return ''; } const name = inviter.title; if (stagedMemberships.length === 1) { return i18n('PendingInvites--revoke-from-singular', { name }); } return i18n('PendingInvites--revoke-from-plural', { number: stagedMemberships.length.toString(), name, }); } function MembersPendingAdminApproval({ conversation, i18n, memberships, setStagedMemberships, }: Readonly<{ conversation: ConversationType; i18n: LocalizerType; memberships: ReadonlyArray; setStagedMemberships: (stagedMembership: Array) => void; }>) { return ( {memberships.map(membership => ( } label={membership.member.title} actions={ conversation.areWeAdmin ? ( <> ) : null } /> ))}
{i18n('PendingRequests--info', [conversation.title])}
); } function MembersPendingProfileKey({ conversation, i18n, members, memberships, ourUuid, setStagedMemberships, }: Readonly<{ conversation: ConversationType; i18n: LocalizerType; members: Array; memberships: ReadonlyArray; ourUuid: string; setStagedMemberships: (stagedMembership: Array) => void; }>) { const groupedPendingMemberships = _.groupBy( memberships, membership => membership.metadata.addedByUserId ); const { [ourUuid]: ourPendingMemberships, ...otherPendingMembershipGroups } = groupedPendingMemberships; const otherPendingMemberships = Object.keys(otherPendingMembershipGroups) .map(id => members.find(member => member.id === id)) .filter((member): member is ConversationType => member !== undefined) .map(member => ({ member, pendingMemberships: otherPendingMembershipGroups[member.id], })); return ( {ourPendingMemberships && ( {ourPendingMemberships.map(membership => ( } label={membership.member.title} actions={ conversation.areWeAdmin ? ( { setStagedMemberships([ { type: StageType.REVOKE_INVITE, membership, }, ]); }} /> ) : null } /> ))} )} {otherPendingMemberships.length > 0 && ( {otherPendingMemberships.map(({ member, pendingMemberships }) => ( } label={member.title} right={i18n('PendingInvites--invited-count', [ pendingMemberships.length.toString(), ])} actions={ conversation.areWeAdmin ? ( { setStagedMemberships( pendingMemberships.map(membership => ({ type: StageType.REVOKE_INVITE, membership, })) ); }} /> ) : null } /> ))} )}
{i18n('PendingInvites--info')}
); }