diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 04e4882ba0..139b4b64bb 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -6236,6 +6236,14 @@ "messageformat": "Hello!", "description": "Text of message bubble showing preview of user's member label" }, + "icu:ConversationDetails--member-label--list-header": { + "messageformat": "Group members with labels", + "description": "Title for section with all the other members in the group with labels" + }, + "icu:ConversationDetails--member-label--no-members": { + "messageformat": "No other members have labels", + "description": "Placeholder for 'other members with labels' section when there are zero other members with labels" + }, "icu:ConversationDetails--member-label--saving": { "messageformat": "Saving changes...", "description": "Accessibility label for button with spinner as we save the user's member label." diff --git a/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.stories.tsx b/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.stories.tsx index 9989889d5e..bc0875f3d0 100644 --- a/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.stories.tsx +++ b/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.stories.tsx @@ -4,6 +4,8 @@ import * as React from 'react'; import { action } from '@storybook/addon-actions'; import type { Meta } from '@storybook/react'; +import { sample } from 'lodash'; + import type { PropsType } from './GroupMemberLabelEditor.dom.js'; import { GroupMemberLabelEditor } from './GroupMemberLabelEditor.dom.js'; import { getDefaultConversation } from '../../../test-helpers/getDefaultConversation.std.js'; @@ -12,6 +14,7 @@ import { getFakeBadge } from '../../../test-helpers/getFakeBadge.std.js'; import { SECOND } from '../../../util/durations/constants.std.js'; import { sleep } from '../../../util/sleep.std.js'; import { SignalService as Proto } from '../../../protobuf/index.std.js'; +import { ContactNameColors } from '../../../types/Colors.std.js'; const { i18n } = window.SignalContext; @@ -26,6 +29,7 @@ const createProps = (): PropsType => ({ existingLabelString: 'Good Memory', getPreferredBadge: () => undefined, i18n, + membersWithLabel: [], ourColor: '160', popPanelForConversation: action('popPanelForConversation'), theme: ThemeType.light, @@ -120,3 +124,81 @@ export function PermissionsRestrictedButAdmin(): React.JSX.Element { /> ); } + +export function NoMembersWithLabel(): React.JSX.Element { + const props: PropsType = createProps(); + + return ; +} + +export function AFewMembersWithLabel(): React.JSX.Element { + const props: PropsType = createProps(); + + return ( + ({ + member: getDefaultConversation(), + isAdmin: i <= 2, + labelEmoji: sample([ + '⚫', + '❤️', + '🫥', + '🤍', + '2️⃣', + '3️⃣', + '🥂', + '🎊', + '➕', + '😵‍💫', + '🚲', + '🐶', + '🐱', + '🏠', + ]), + labelString: + i % 2 === 0 + ? `Label number long long long long long long long long long ${i}` + : `Label member ${i}`, + contactNameColor, + }) + )} + /> + ); +} + +export function LotsOfMembersWithLabel(): React.JSX.Element { + const props: PropsType = createProps(); + + return ( + ({ + member: getDefaultConversation(), + isAdmin: i <= 6, + labelEmoji: sample([ + '⚫', + '❤️', + '🫥', + '🤍', + '2️⃣', + '3️⃣', + '🥂', + '🎊', + '➕', + '😵‍💫', + '🚲', + '🐶', + '🐱', + '🏠', + ]), + labelString: + i % 2 === 0 + ? `Label number long long long long long long long long long ${i}` + : `Label member ${i}`, + contactNameColor, + }))} + /> + ); +} diff --git a/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx b/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx index bd19a3fffc..f98996df31 100644 --- a/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx +++ b/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx @@ -36,6 +36,9 @@ import type { } from '../../../state/ducks/conversations.preload.js'; import type { LocalizerType, ThemeType } from '../../../types/Util.std.js'; import type { PreferredBadgeSelectorType } from '../../../state/selectors/badges.preload.js'; +import { Avatar, AvatarSize } from '../../Avatar.dom.js'; +import { UserText } from '../../UserText.dom.js'; +import { GroupMemberLabel } from '../ContactName.dom.js'; export type PropsDataType = { existingLabelEmoji: string | undefined; @@ -43,6 +46,13 @@ export type PropsDataType = { group: ConversationType; i18n: LocalizerType; me: ConversationType; + membersWithLabel: Array<{ + contactNameColor: string; + isAdmin: boolean; + labelEmoji: string | undefined; + labelString: string; + member: ConversationType; + }>; ourColor: string | undefined; theme: ThemeType; }; @@ -68,6 +78,7 @@ export function GroupMemberLabelEditor({ existingLabelString, getPreferredBadge, i18n, + membersWithLabel, ourColor, popPanelForConversation, theme, @@ -113,127 +124,207 @@ export function GroupMemberLabelEditor({ }, [group, isShowingPermissionsError, setIsShowingPermissionsError]); return ( -
-
- setEmojiPickerOpen(open)} - placement="bottom" - onSelectEmoji={data => { - const newEmoji = getEmojiVariantByKey(data.variantKey)?.value; +
+
+
+ setEmojiPickerOpen(open)} + placement="bottom" + onSelectEmoji={data => { + const newEmoji = getEmojiVariantByKey(data.variantKey)?.value; - setLabelEmoji(newEmoji); - }} - closeOnSelect - theme={theme} - > - - - } - maxLengthCount={STRING_GRAPHEME_LIMIT} - maxByteCount={STRING_BYTE_LIMIT} - moduleClassName="GroupMemberLabelEditor" - onChange={value => { - if (!value) { - setLabelEmoji(undefined); + setLabelEmoji(newEmoji); + }} + closeOnSelect + theme={theme} + > + + } + maxLengthCount={STRING_GRAPHEME_LIMIT} + maxByteCount={STRING_BYTE_LIMIT} + moduleClassName="GroupMemberLabelEditor" + onChange={value => { + if (!value) { + setLabelEmoji(undefined); + } - // Replace all whitespace with basic space - setLabelString(value.replace(/\s/g, ' ')); - }} - ref={undefined} - placeholder={i18n( - 'icu:ConversationDetails--member-label--placeholder' - )} - value={labelString} - whenToShowRemainingCount={20} - /> -
-
- {i18n('icu:ConversationDetails--member-label--description')} -
-
- {i18n('icu:ConversationDetails--member-label--preview')} + // Replace all whitespace with basic space + setLabelString(value.replace(/\s/g, ' ')); + }} + ref={undefined} + placeholder={i18n( + 'icu:ConversationDetails--member-label--placeholder' + )} + value={labelString} + whenToShowRemainingCount={20} + /> +
+ {i18n('icu:ConversationDetails--member-label--description')} +
+
+ {i18n('icu:ConversationDetails--member-label--preview')} +
+
+
} + doubleCheckMissingQuoteReference={noop} + messageExpanded={noop} + checkForAccount={noop} + startConversation={noop} + showConversation={noop} + openGiftBadge={noop} + pushPanelForConversation={noop} + retryMessageSend={noop} + sendPollVote={noop} + endPoll={noop} + showContactModal={noop} + showSpoiler={noop} + cancelAttachmentDownload={noop} + kickOffAttachmentDownload={noop} + markAttachmentAsCorrupted={noop} + saveAttachment={noop} + saveAttachments={noop} + showLightbox={noop} + showLightboxForViewOnceMedia={noop} + scrollToQuotedMessage={noop} + showAttachmentDownloadStillInProgressToast={noop} + showExpiredIncomingTapToViewToast={noop} + showExpiredOutgoingTapToViewToast={noop} + showMediaNoLongerAvailableToast={noop} + showTapToViewNotAvailableModal={noop} + viewStory={noop} + onToggleSelect={noop} + onReplyToMessage={noop} + /> +
+
+ {i18n('icu:ConversationDetails--member-label--list-header')} +
+
+ {membersWithLabel.length === 0 && ( +
+ {i18n('icu:ConversationDetails--member-label--no-members')} +
+ )} + {membersWithLabel.map(membership => { + const { + contactNameColor, + isAdmin, + labelEmoji: memberLabelEmoji, + labelString: memberLabelString, + member, + } = membership; + + return ( +
+
+ +
+
+
+ +
+ {memberLabelString && contactNameColor && ( +
+ +
+ )} +
+ {isAdmin && ( +
+ {i18n('icu:GroupV2--admin')} +
+ )} +
+ ); + })} +
+
-
} - doubleCheckMissingQuoteReference={noop} - messageExpanded={noop} - checkForAccount={noop} - startConversation={noop} - showConversation={noop} - openGiftBadge={noop} - pushPanelForConversation={noop} - retryMessageSend={noop} - sendPollVote={noop} - endPoll={noop} - showContactModal={noop} - showSpoiler={noop} - cancelAttachmentDownload={noop} - kickOffAttachmentDownload={noop} - markAttachmentAsCorrupted={noop} - saveAttachment={noop} - saveAttachments={noop} - showLightbox={noop} - showLightboxForViewOnceMedia={noop} - scrollToQuotedMessage={noop} - showAttachmentDownloadStillInProgressToast={noop} - showExpiredIncomingTapToViewToast={noop} - showExpiredOutgoingTapToViewToast={noop} - showMediaNoLongerAvailableToast={noop} - showTapToViewNotAvailableModal={noop} - viewStory={noop} - onToggleSelect={noop} - onReplyToMessage={noop} - /> -
- -
{ + const { aci, isAdmin, labelEmoji, labelString } = membership; + + if (aci === me.serviceId) { + return; + } + + if (!labelString) { + return; + } + + const member = conversationSelector(aci); + if (!member) { + log.warn( + 'Group member was not found, excluding from members with labels' + ); + return; + } + const contactNameColor = memberColors.get(member.id); + if (!contactNameColor) { + log.warn( + 'Color not found for group member, excluding from members with labels' + ); + return; + } + + return { + contactNameColor, + isAdmin, + labelEmoji, + labelString, + member, + }; + }) + .filter(isNotNil); + return (