236 lines
6.0 KiB
TypeScript
236 lines
6.0 KiB
TypeScript
// Copyright 2025 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import React, { useMemo } from 'react';
|
|
import type { ReactNode } from 'react';
|
|
import { take } from 'lodash';
|
|
|
|
import { I18n } from './I18n';
|
|
import type { LocalizerType } from '../types/Util';
|
|
import { UserText } from './UserText';
|
|
import type { GroupV2Membership } from './conversation/conversation-details/ConversationDetailsMembershipList';
|
|
|
|
type PropsType = {
|
|
i18n: LocalizerType;
|
|
nameClassName?: string;
|
|
memberships: ReadonlyArray<GroupV2Membership>;
|
|
invitesCount?: number;
|
|
onOtherMembersClick?: () => void;
|
|
};
|
|
|
|
// Define renderClickableButton outside component to avoid nested component definitions
|
|
function renderClickableButton(
|
|
parts: ReactNode,
|
|
onOtherMembersClick?: () => void
|
|
): JSX.Element {
|
|
return (
|
|
<button
|
|
className="module-conversation-hero__members-count__button"
|
|
type="button"
|
|
onClick={ev => {
|
|
ev.preventDefault();
|
|
if (onOtherMembersClick) {
|
|
onOtherMembersClick();
|
|
}
|
|
}}
|
|
>
|
|
{parts}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function MemberList({
|
|
otherMemberNames,
|
|
firstThreeMemberNames,
|
|
areWeInGroup,
|
|
i18n,
|
|
onOtherMembersClick,
|
|
}: {
|
|
otherMemberNames: ReadonlyArray<string | undefined>;
|
|
firstThreeMemberNames: Array<JSX.Element>;
|
|
areWeInGroup: boolean;
|
|
i18n: LocalizerType;
|
|
onOtherMembersClick?: () => void;
|
|
}): JSX.Element {
|
|
if (areWeInGroup) {
|
|
if (otherMemberNames.length === 0) {
|
|
return (
|
|
<I18n i18n={i18n} id="icu:ConversationHero--group-members-only-you" />
|
|
);
|
|
}
|
|
|
|
if (otherMemberNames.length === 1) {
|
|
return (
|
|
<I18n
|
|
i18n={i18n}
|
|
id="icu:ConversationHero--group-members-one-and-you"
|
|
components={{
|
|
member: firstThreeMemberNames[0],
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (otherMemberNames.length === 2) {
|
|
return (
|
|
<I18n
|
|
i18n={i18n}
|
|
id="icu:ConversationHero--group-members-two-and-you"
|
|
components={{
|
|
member1: firstThreeMemberNames[0],
|
|
member2: firstThreeMemberNames[1],
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// For 3+ members, "you" is looped in with "others", not shown separately
|
|
const remainingCount = otherMemberNames.length + Number(areWeInGroup) - 3;
|
|
return (
|
|
<I18n
|
|
i18n={i18n}
|
|
id="icu:ConversationHero--group-members-other-and-you"
|
|
components={{
|
|
member1: firstThreeMemberNames[0],
|
|
member2: firstThreeMemberNames[1],
|
|
member3: firstThreeMemberNames[2],
|
|
clickable: (parts: ReactNode) =>
|
|
renderClickableButton(parts, onOtherMembersClick),
|
|
remainingCount,
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// When the user is not in the group
|
|
|
|
if (otherMemberNames.length === 0) {
|
|
return <I18n i18n={i18n} id="icu:ConversationHero--group-members-zero" />;
|
|
}
|
|
|
|
if (otherMemberNames.length === 1) {
|
|
return (
|
|
<I18n
|
|
i18n={i18n}
|
|
id="icu:ConversationHero--group-members-one"
|
|
components={{
|
|
member: firstThreeMemberNames[0],
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (otherMemberNames.length === 2) {
|
|
return (
|
|
<I18n
|
|
i18n={i18n}
|
|
id="icu:ConversationHero--group-members-two"
|
|
components={{
|
|
member1: firstThreeMemberNames[0],
|
|
member2: firstThreeMemberNames[1],
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (otherMemberNames.length === 3) {
|
|
return (
|
|
<I18n
|
|
i18n={i18n}
|
|
id="icu:ConversationHero--group-members-three"
|
|
components={{
|
|
member1: firstThreeMemberNames[0],
|
|
member2: firstThreeMemberNames[1],
|
|
member3: firstThreeMemberNames[2],
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// More than 3 members
|
|
const remainingCount = otherMemberNames.length - 3;
|
|
return (
|
|
<I18n
|
|
i18n={i18n}
|
|
id="icu:ConversationHero--group-members-other"
|
|
components={{
|
|
member1: firstThreeMemberNames[0],
|
|
member2: firstThreeMemberNames[1],
|
|
member3: firstThreeMemberNames[2],
|
|
clickable: (parts: ReactNode) =>
|
|
renderClickableButton(parts, onOtherMembersClick),
|
|
remainingCount,
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function GroupMembersNames({
|
|
i18n,
|
|
nameClassName,
|
|
memberships,
|
|
invitesCount,
|
|
onOtherMembersClick,
|
|
}: PropsType): JSX.Element {
|
|
const areWeInGroup = useMemo(() => {
|
|
return memberships.some(({ member }) => member.isMe);
|
|
}, [memberships]);
|
|
|
|
const otherMemberNames = useMemo(() => {
|
|
return memberships
|
|
.filter(({ member }) => !member.isMe)
|
|
.map(({ member }) => member.titleShortNoDefault);
|
|
}, [memberships]);
|
|
|
|
// Take the first 3 members for display, prioritizing defined names
|
|
// "Unknown" is the fallback name if we never got the right profileKey
|
|
// for a user, or haven't fetched their profile yet.
|
|
const firstThreeMembers = useMemo(() => {
|
|
return take(
|
|
[...otherMemberNames].sort((a, b) => {
|
|
if (a === undefined) {
|
|
return 1;
|
|
}
|
|
if (b === undefined) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}),
|
|
3
|
|
).map((name, i) => (
|
|
// We cannot guarantee uniqueness of member names
|
|
// eslint-disable-next-line react/no-array-index-key
|
|
<strong key={i} className={nameClassName}>
|
|
<UserText text={name ?? i18n('icu:unknownContactShort')} />
|
|
</strong>
|
|
));
|
|
}, [otherMemberNames, nameClassName, i18n]);
|
|
|
|
const memberListElement = (
|
|
<MemberList
|
|
otherMemberNames={otherMemberNames}
|
|
firstThreeMemberNames={firstThreeMembers}
|
|
areWeInGroup={areWeInGroup}
|
|
i18n={i18n}
|
|
onOtherMembersClick={onOtherMembersClick}
|
|
/>
|
|
);
|
|
|
|
// If there are invited members, wrap in the "(+1 invited)" format
|
|
if (invitesCount && invitesCount > 0) {
|
|
return (
|
|
<I18n
|
|
i18n={i18n}
|
|
id="icu:ConversationHero--member-list-and-invited"
|
|
components={{
|
|
memberList: memberListElement,
|
|
invitesCount,
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Otherwise just return the member list
|
|
return memberListElement;
|
|
}
|