// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; import _ from 'lodash'; import type { ConversationType } from '../../../state/ducks/conversations'; import type { LocalizerType, ThemeType } from '../../../types/Util'; import type { PreferredBadgeSelectorType } from '../../../state/selectors/badges'; import type { AciString } from '../../../types/ServiceId'; import { Avatar, AvatarSize } from '../../Avatar'; import { ConfirmationDialog } from '../../ConfirmationDialog'; import { PanelSection } from './PanelSection'; import { PanelRow } from './PanelRow'; import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon'; import { isAccessControlEnabled } from '../../../groups/util'; import { Tabs } from '../../Tabs'; import { assertDev } from '../../../util/assert'; export type PropsDataType = { readonly conversation?: ConversationType; readonly getPreferredBadge: PreferredBadgeSelectorType; readonly i18n: LocalizerType; readonly ourAci: AciString; readonly pendingApprovalMemberships: ReadonlyArray; readonly pendingMemberships: ReadonlyArray; readonly theme: ThemeType; }; type PropsActionType = { readonly approvePendingMembershipFromGroupV2: ( conversationId: string, memberId: string ) => void; readonly revokePendingMembershipsFromGroupV2: ( conversationId: string, memberIds: ReadonlyArray ) => void; }; export type PropsType = PropsDataType & PropsActionType; export type GroupV2PendingMembership = { metadata: { addedByUserId?: AciString; }; 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 function PendingInvites({ approvePendingMembershipFromGroupV2, conversation, getPreferredBadge, i18n, ourAci, pendingMemberships, pendingApprovalMemberships, revokePendingMembershipsFromGroupV2, theme, }: PropsType): JSX.Element { if (!conversation || !ourAci) { throw new Error('PendingInvites rendered without a conversation or ourAci'); } const [stagedMemberships, setStagedMemberships] = React.useState | null>(null); return (
{({ selectedTab }) => ( <> {selectedTab === Tab.Requests ? ( ) : null} {selectedTab === Tab.Pending ? ( ) : null} )} {stagedMemberships && stagedMemberships.length && ( setStagedMemberships(null)} ourAci={ourAci} revokePendingMembershipsFromGroupV2={ revokePendingMembershipsFromGroupV2 } stagedMemberships={stagedMemberships} /> )}
); } function MembershipActionConfirmation({ approvePendingMembershipFromGroupV2, conversation, i18n, members, onClose, ourAci, revokePendingMembershipsFromGroupV2, stagedMemberships, }: { approvePendingMembershipFromGroupV2: ( conversationId: string, memberId: string ) => void; conversation: ConversationType; i18n: LocalizerType; members: ReadonlyArray; onClose: () => void; ourAci: AciString; revokePendingMembershipsFromGroupV2: ( conversationId: string, memberIds: ReadonlyArray ) => void; stagedMemberships: ReadonlyArray; }) { const revokeStagedMemberships = () => { if (!stagedMemberships) { return; } revokePendingMembershipsFromGroupV2( conversation.id, stagedMemberships.map(({ membership }) => membership.member.id) ); }; const approveStagedMembership = () => { if (!stagedMemberships) { return; } approvePendingMembershipFromGroupV2( conversation.id, stagedMemberships[0].membership.member.id ); }; const membershipType = stagedMemberships[0].type; const modalAction = membershipType === StageType.APPROVE_REQUEST ? approveStagedMembership : revokeStagedMemberships; let modalActionText = i18n('icu:PendingInvites--revoke'); if (membershipType === StageType.APPROVE_REQUEST) { modalActionText = i18n('icu:PendingRequests--approve'); } else if (membershipType === StageType.DENY_REQUEST) { modalActionText = i18n('icu:PendingRequests--deny'); } else if (membershipType === StageType.REVOKE_INVITE) { modalActionText = i18n('icu:PendingInvites--revoke'); } return ( {getConfirmationMessage({ conversation, i18n, members, ourAci, stagedMemberships, })} ); } function getConfirmationMessage({ conversation, i18n, members, ourAci, stagedMemberships, }: Readonly<{ conversation: ConversationType; i18n: LocalizerType; members: ReadonlyArray; ourAci: AciString; 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 isAccessControlEnabled(conversation.accessControlAddFromInviteLink) ? i18n('icu:PendingRequests--deny-for--with-link', { name: firstMembership.member.title, }) : i18n('icu:PendingRequests--deny-for', { name: firstMembership.member.title, }); } if (membershipType === StageType.APPROVE_REQUEST) { return i18n('icu: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 === ourAci; if (invitedByUs) { return i18n('icu:PendingInvites--revoke-for', { name: firstPendingMembership.member.title, }); } const inviter = members.find( ({ serviceId }) => serviceId === firstPendingMembership.metadata.addedByUserId ); if (inviter === undefined) { return ''; } const name = inviter.title; return i18n('icu:PendingInvites--revoke-from', { number: stagedMemberships.length, name, }); } function MembersPendingAdminApproval({ conversation, getPreferredBadge, i18n, memberships, setStagedMemberships, theme, }: Readonly<{ conversation: ConversationType; getPreferredBadge: PreferredBadgeSelectorType; i18n: LocalizerType; memberships: ReadonlyArray; setStagedMemberships: (stagedMembership: Array) => void; theme: ThemeType; }>) { return ( {memberships.map(membership => ( } label={ {membership.member.title} } actions={ conversation.areWeAdmin ? ( <> ) : null } /> ))}
{i18n('icu:PendingRequests--info', { name: conversation.title, })}
); } function MembersPendingProfileKey({ conversation, i18n, members, memberships, ourAci, setStagedMemberships, getPreferredBadge, theme, }: Readonly<{ conversation: ConversationType; getPreferredBadge: PreferredBadgeSelectorType; i18n: LocalizerType; members: ReadonlyArray; memberships: ReadonlyArray; ourAci: AciString; setStagedMemberships: (stagedMembership: Array) => void; theme: ThemeType; }>) { const groupedPendingMemberships = _.groupBy( memberships, membership => membership.metadata.addedByUserId ); const { [ourAci]: ourPendingMemberships, ...otherPendingMembershipGroups } = groupedPendingMemberships; const otherPendingMemberships = Object.keys(otherPendingMembershipGroups) .map(id => members.find(member => member.serviceId === id)) .filter((member): member is ConversationType => member !== undefined) .map(member => { assertDev( member.serviceId, 'We just verified that member has serviceId above' ); return { member, pendingMemberships: otherPendingMembershipGroups[member.serviceId], }; }); 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('icu:PendingInvites--invited-count', { number: pendingMemberships.length, })} actions={ conversation.areWeAdmin ? ( { setStagedMemberships( pendingMemberships.map(membership => ({ type: StageType.REVOKE_INVITE, membership, })) ); }} /> ) : null } /> ))} )}
{i18n('icu:PendingInvites--info')}
); }