mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-08 08:58:38 +01:00
Support for Group Member Labels
This commit is contained in:
@@ -34,6 +34,8 @@ const SemverKeys = [
|
||||
'desktop.callQualitySurvey.prod',
|
||||
'desktop.donationPaypal.beta',
|
||||
'desktop.donationPaypal.prod',
|
||||
'desktop.groupMemberLabels.edit.beta',
|
||||
'desktop.groupMemberLabels.edit.prod',
|
||||
'desktop.pinnedMessages.receive.beta',
|
||||
'desktop.pinnedMessages.receive.prod',
|
||||
'desktop.pinnedMessages.send.beta',
|
||||
|
||||
@@ -2,28 +2,31 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import lodash from 'lodash';
|
||||
import type {
|
||||
ConversationType,
|
||||
ShowConversationType,
|
||||
} from '../state/ducks/conversations.preload.js';
|
||||
import type { ShowConversationType } from '../state/ducks/conversations.preload.js';
|
||||
import { I18n } from './I18n.dom.js';
|
||||
import type { LocalizerType, ThemeType } from '../types/Util.std.js';
|
||||
import { Modal } from './Modal.dom.js';
|
||||
import { ConversationListItem } from './conversationList/ConversationListItem.dom.js';
|
||||
|
||||
const { noop } = lodash;
|
||||
import { Avatar, AvatarSize } from './Avatar.dom.js';
|
||||
import { GroupMemberLabel } from './conversation/ContactName.dom.js';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges.preload.js';
|
||||
import { tw } from '../axo/tw.dom.js';
|
||||
import type { AdminMembershipType } from '../state/selectors/conversations.dom.js';
|
||||
import { UserText } from './UserText.dom.js';
|
||||
|
||||
type PropsType = {
|
||||
groupAdmins: Array<ConversationType>;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
groupAdmins: Array<AdminMembershipType>;
|
||||
memberColors: Map<string, string>;
|
||||
i18n: LocalizerType;
|
||||
showConversation: ShowConversationType;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
||||
export function AnnouncementsOnlyGroupBanner({
|
||||
getPreferredBadge,
|
||||
groupAdmins,
|
||||
i18n,
|
||||
memberColors,
|
||||
showConversation,
|
||||
theme,
|
||||
}: PropsType): React.JSX.Element {
|
||||
@@ -33,25 +36,58 @@ export function AnnouncementsOnlyGroupBanner({
|
||||
<>
|
||||
{isShowingAdmins && (
|
||||
<Modal
|
||||
modalName="AnnouncmentsOnlyGroupBanner"
|
||||
i18n={i18n}
|
||||
hasXButton
|
||||
modalName="AnnouncmentsOnlyGroupBanner"
|
||||
onClose={() => setIsShowingAdmins(false)}
|
||||
title={i18n('icu:AnnouncementsOnlyGroupBanner--modal')}
|
||||
>
|
||||
{groupAdmins.map(admin => (
|
||||
<ConversationListItem
|
||||
{...admin}
|
||||
draftPreview={undefined}
|
||||
i18n={i18n}
|
||||
lastMessage={undefined}
|
||||
lastUpdated={undefined}
|
||||
onClick={() => {
|
||||
showConversation({ conversationId: admin.id });
|
||||
}}
|
||||
onMouseDown={noop}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
{groupAdmins.map(admin => {
|
||||
const { member, labelEmoji, labelString } = admin;
|
||||
const contactNameColor = memberColors.get(member.id);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
showConversation({ conversationId: member.id });
|
||||
}}
|
||||
className={tw('flex flex-row items-center p-2')}
|
||||
>
|
||||
<div className={tw('pe-3')}>
|
||||
<Avatar
|
||||
conversationType="direct"
|
||||
badge={getPreferredBadge(member.badges)}
|
||||
i18n={i18n}
|
||||
size={AvatarSize.THIRTY_SIX}
|
||||
theme={theme}
|
||||
{...member}
|
||||
/>
|
||||
</div>
|
||||
<div className={tw('flex flex-col items-start')}>
|
||||
<div>
|
||||
<UserText
|
||||
text={member.isMe ? i18n('icu:you') : member.title}
|
||||
/>
|
||||
</div>
|
||||
{labelString && contactNameColor && (
|
||||
<div>
|
||||
<GroupMemberLabel
|
||||
contactNameColor={contactNameColor}
|
||||
contactLabel={{
|
||||
labelEmoji,
|
||||
labelString,
|
||||
}}
|
||||
context="list"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Modal>
|
||||
)}
|
||||
<div className="AnnouncementsOnlyGroupBanner__banner">
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ThemeType } from '../types/Util.std.js';
|
||||
import { Theme } from '../util/theme.std.js';
|
||||
import { UserText } from './UserText.dom.js';
|
||||
import { SharedGroupNames } from './SharedGroupNames.dom.js';
|
||||
import type { ContactModalStateType } from '../state/ducks/globalModals.preload.js';
|
||||
|
||||
export type CallLinkPendingParticipantModalProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
@@ -21,7 +22,7 @@ export type CallLinkPendingParticipantModalProps = {
|
||||
readonly denyUser: (payload: PendingUserActionPayloadType) => void;
|
||||
readonly onClose: () => void;
|
||||
readonly sharedGroupNames: ReadonlyArray<string>;
|
||||
readonly toggleAboutContactModal: (conversationId: string) => void;
|
||||
readonly toggleAboutContactModal: (options: ContactModalStateType) => void;
|
||||
};
|
||||
|
||||
export function CallLinkPendingParticipantModal({
|
||||
@@ -75,7 +76,7 @@ export function CallLinkPendingParticipantModal({
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleAboutContactModal(conversation.id);
|
||||
toggleAboutContactModal({ contactId: conversation.id });
|
||||
}}
|
||||
className="CallLinkPendingParticipantModal__NameButton"
|
||||
>
|
||||
|
||||
@@ -12,13 +12,47 @@ import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext.st
|
||||
import { fakeDraftAttachment } from '../test-helpers/fakeAttachment.std.js';
|
||||
import { landscapeGreenUrl } from '../storybook/Fixtures.std.js';
|
||||
import { RecordingState } from '../types/AudioRecorder.std.js';
|
||||
import { ConversationColors } from '../types/Colors.std.js';
|
||||
import { ContactNameColors, ConversationColors } from '../types/Colors.std.js';
|
||||
import { getDefaultConversation } from '../test-helpers/getDefaultConversation.std.js';
|
||||
import { PaymentEventKind } from '../types/Payment.std.js';
|
||||
import { EmojiSkinTone } from './fun/data/emojis.std.js';
|
||||
import { isNotNil } from '../util/isNotNil.std.js';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
const groupAdmins = [
|
||||
{
|
||||
member: getDefaultConversation(),
|
||||
labelEmoji: undefined,
|
||||
labelString: undefined,
|
||||
},
|
||||
{
|
||||
member: getDefaultConversation(),
|
||||
labelEmoji: '✅',
|
||||
labelString: 'Planner',
|
||||
},
|
||||
{
|
||||
member: getDefaultConversation(),
|
||||
labelEmoji: '#',
|
||||
labelString: 'Invalid Emoji',
|
||||
},
|
||||
{
|
||||
member: getDefaultConversation(),
|
||||
labelEmoji: undefined,
|
||||
labelString: 'No Emoji',
|
||||
},
|
||||
];
|
||||
const memberColors = new Map(
|
||||
groupAdmins
|
||||
.map((admin, i): [string, string] | null => {
|
||||
if (!admin.member.id) {
|
||||
return null;
|
||||
}
|
||||
return [admin.member.id?.toString(), ContactNameColors[i]];
|
||||
})
|
||||
.filter(isNotNil)
|
||||
);
|
||||
|
||||
export default {
|
||||
title: 'Components/CompositionArea',
|
||||
decorators: [
|
||||
@@ -102,7 +136,8 @@ export default {
|
||||
announcementsOnly: false,
|
||||
areWeAdmin: false,
|
||||
areWePendingApproval: false,
|
||||
groupAdmins: [],
|
||||
groupAdmins,
|
||||
memberColors,
|
||||
cancelJoinRequest: action('cancelJoinRequest'),
|
||||
showConversation: action('showConversation'),
|
||||
isSmsOnlyOrUnregistered: false,
|
||||
|
||||
@@ -112,7 +112,11 @@ export type OwnProps = Readonly<{
|
||||
errorDialogAudioRecorderType: ErrorDialogAudioRecorderType | null;
|
||||
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
|
||||
focusCounter: number;
|
||||
groupAdmins: Array<ConversationType>;
|
||||
groupAdmins: Array<{
|
||||
member: ConversationType;
|
||||
labelEmoji: string | undefined;
|
||||
labelString: string | undefined;
|
||||
}>;
|
||||
groupVersion: 1 | 2 | null;
|
||||
i18n: LocalizerType;
|
||||
imageToBlurHash: typeof imageToBlurHash;
|
||||
@@ -126,6 +130,7 @@ export type OwnProps = Readonly<{
|
||||
lastEditableMessageId: string | null;
|
||||
recordingState: RecordingState;
|
||||
messageCompositionId: string;
|
||||
memberColors: Map<string, string>;
|
||||
shouldHidePopovers: boolean | null;
|
||||
isMuted: boolean;
|
||||
isSmsOnlyOrUnregistered: boolean | null;
|
||||
@@ -321,6 +326,7 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
announcementsOnly,
|
||||
areWeAdmin,
|
||||
groupAdmins,
|
||||
memberColors,
|
||||
cancelJoinRequest,
|
||||
showConversation,
|
||||
// SMS-only contacts
|
||||
@@ -1005,8 +1011,10 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
if (announcementsOnly && !areWeAdmin) {
|
||||
return (
|
||||
<AnnouncementsOnlyGroupBanner
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
groupAdmins={groupAdmins}
|
||||
i18n={i18n}
|
||||
memberColors={memberColors}
|
||||
showConversation={showConversation}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
@@ -558,7 +558,7 @@ export function NotificationProfilesHome({
|
||||
i18n={i18n}
|
||||
isEditing
|
||||
onBack={() => setPage(HomePage.Edit)}
|
||||
onNext={() => setPage(HomePage.Edit)} // TODO: probably don't show Next button?
|
||||
onNext={() => setPage(HomePage.Edit)}
|
||||
onSetIsEnabled={(scheduleEnabled: boolean) => {
|
||||
const newProfile = {
|
||||
...profile,
|
||||
|
||||
@@ -61,18 +61,23 @@ export default {
|
||||
isSignalConnection: { control: { type: 'boolean' } },
|
||||
},
|
||||
args: {
|
||||
canAddLabel: false,
|
||||
contact: conversation,
|
||||
contactLabelEmoji: undefined,
|
||||
contactLabelString: undefined,
|
||||
contactNameColor: undefined,
|
||||
fromOrAddedByTrustedContact: false,
|
||||
i18n,
|
||||
isSignalConnection: false,
|
||||
isEditMemberLabelEnabled: true,
|
||||
onClose: action('onClose'),
|
||||
onOpenNotePreviewModal: action('onOpenNotePreviewModal'),
|
||||
toggleSignalConnectionsModal: action('toggleSignalConnections'),
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
toggleProfileNameWarningModal: action('toggleProfileNameWarningModal'),
|
||||
startAvatarDownload: action('startAvatarDownload'),
|
||||
pendingAvatarDownload: false,
|
||||
conversation,
|
||||
sharedGroupNames: [],
|
||||
fromOrAddedByTrustedContact: false,
|
||||
isSignalConnection: false,
|
||||
startAvatarDownload: action('startAvatarDownload'),
|
||||
toggleProfileNameWarningModal: action('toggleProfileNameWarningModal'),
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
toggleSignalConnectionsModal: action('toggleSignalConnections'),
|
||||
},
|
||||
} satisfies ComponentMeta<PropsType>;
|
||||
|
||||
@@ -81,27 +86,80 @@ export function Defaults(args: PropsType): React.JSX.Element {
|
||||
}
|
||||
|
||||
export function Me(args: PropsType): React.JSX.Element {
|
||||
return <AboutContactModal {...args} conversation={me} />;
|
||||
return <AboutContactModal {...args} contact={me} />;
|
||||
}
|
||||
|
||||
export function MeWithLabel(args: PropsType): React.JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...{
|
||||
...args,
|
||||
contactLabelEmoji: '🐝',
|
||||
contactLabelString: 'Worker Bee',
|
||||
contactNameColor: '270',
|
||||
}}
|
||||
contact={me}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function MeWithInvalidLabelEmoji(args: PropsType): React.JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...{
|
||||
...args,
|
||||
contactLabelEmoji: '@',
|
||||
contactLabelString: 'Worker Bee',
|
||||
contactNameColor: '270',
|
||||
}}
|
||||
contact={me}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function MeWithAddLabel(args: PropsType): React.JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...{
|
||||
...args,
|
||||
canAddLabel: true,
|
||||
}}
|
||||
contact={me}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function MeWithAddLabelEditDisabled(args: PropsType): React.JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...{
|
||||
...args,
|
||||
canAddLabel: true,
|
||||
}}
|
||||
contact={me}
|
||||
isEditMemberLabelEnabled={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Verified(args: PropsType): React.JSX.Element {
|
||||
return <AboutContactModal {...args} conversation={verifiedConversation} />;
|
||||
return <AboutContactModal {...args} contact={verifiedConversation} />;
|
||||
}
|
||||
|
||||
export function Blocked(args: PropsType): React.JSX.Element {
|
||||
return <AboutContactModal {...args} conversation={blockedConversation} />;
|
||||
return <AboutContactModal {...args} contact={blockedConversation} />;
|
||||
}
|
||||
|
||||
export function Pending(args: PropsType): React.JSX.Element {
|
||||
return <AboutContactModal {...args} conversation={pendingConversation} />;
|
||||
return <AboutContactModal {...args} contact={pendingConversation} />;
|
||||
}
|
||||
|
||||
export function NoMessages(args: PropsType): React.JSX.Element {
|
||||
return <AboutContactModal {...args} conversation={noMessages} />;
|
||||
return <AboutContactModal {...args} contact={noMessages} />;
|
||||
}
|
||||
|
||||
export function WithAbout(args: PropsType): React.JSX.Element {
|
||||
return <AboutContactModal {...args} conversation={conversationWithAbout} />;
|
||||
return <AboutContactModal {...args} contact={conversationWithAbout} />;
|
||||
}
|
||||
|
||||
export function SignalConnection(args: PropsType): React.JSX.Element {
|
||||
@@ -110,11 +168,7 @@ export function SignalConnection(args: PropsType): React.JSX.Element {
|
||||
|
||||
export function SystemContact(args: PropsType): React.JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...args}
|
||||
conversation={systemContact}
|
||||
isSignalConnection
|
||||
/>
|
||||
<AboutContactModal {...args} contact={systemContact} isSignalConnection />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,7 +176,7 @@ export function WithSharedGroups(args: PropsType): React.JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...args}
|
||||
conversation={conversationWithSharedGroups}
|
||||
contact={conversationWithSharedGroups}
|
||||
sharedGroupNames={['Axolotl lovers']}
|
||||
isSignalConnection
|
||||
/>
|
||||
@@ -133,7 +187,7 @@ export function DirectFromTrustedContact(args: PropsType): React.JSX.Element {
|
||||
return (
|
||||
<AboutContactModal
|
||||
{...args}
|
||||
conversation={conversation}
|
||||
contact={conversation}
|
||||
fromOrAddedByTrustedContact
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,14 @@ import { About } from './About.dom.js';
|
||||
import { I18n } from '../I18n.dom.js';
|
||||
import { canHaveNicknameAndNote } from '../../util/nicknames.dom.js';
|
||||
import { Tooltip, TooltipPlacement } from '../Tooltip.dom.js';
|
||||
import { useFunEmojiLocalizer } from '../fun/useFunEmojiLocalizer.dom.js';
|
||||
import {
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from '../fun/data/emojis.std.js';
|
||||
import { FunStaticEmoji } from '../fun/FunEmoji.dom.js';
|
||||
import { missingEmojiPlaceholder } from './ContactName.dom.js';
|
||||
|
||||
function muted(parts: Array<string | React.JSX.Element>) {
|
||||
return (
|
||||
@@ -22,11 +30,16 @@ function muted(parts: Array<string | React.JSX.Element>) {
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
canAddLabel: boolean;
|
||||
contact: ConversationType;
|
||||
contactLabelEmoji: string | undefined;
|
||||
contactLabelString: string | undefined;
|
||||
contactNameColor: string | undefined;
|
||||
fromOrAddedByTrustedContact?: boolean;
|
||||
isEditMemberLabelEnabled: boolean;
|
||||
isSignalConnection: boolean;
|
||||
onClose: () => void;
|
||||
onOpenNotePreviewModal: () => void;
|
||||
conversation: ConversationType;
|
||||
fromOrAddedByTrustedContact?: boolean;
|
||||
isSignalConnection: boolean;
|
||||
pendingAvatarDownload?: boolean;
|
||||
sharedGroupNames: ReadonlyArray<string>;
|
||||
startAvatarDownload?: (id: string) => unknown;
|
||||
@@ -37,8 +50,13 @@ export type PropsType = Readonly<{
|
||||
|
||||
export function AboutContactModal({
|
||||
i18n,
|
||||
conversation,
|
||||
canAddLabel,
|
||||
contact,
|
||||
contactLabelEmoji,
|
||||
contactLabelString,
|
||||
contactNameColor,
|
||||
fromOrAddedByTrustedContact,
|
||||
isEditMemberLabelEnabled,
|
||||
isSignalConnection,
|
||||
pendingAvatarDownload,
|
||||
sharedGroupNames,
|
||||
@@ -49,7 +67,7 @@ export function AboutContactModal({
|
||||
onClose,
|
||||
onOpenNotePreviewModal,
|
||||
}: PropsType): React.JSX.Element {
|
||||
const { avatarUrl, hasAvatar, isMe } = conversation;
|
||||
const { avatarUrl, hasAvatar, isMe } = contact;
|
||||
|
||||
// If hasAvatar is true, we show the download button instead of blur
|
||||
const enableClickToLoad = !avatarUrl && !isMe && hasAvatar;
|
||||
@@ -64,11 +82,11 @@ export function AboutContactModal({
|
||||
}
|
||||
return () => {
|
||||
if (!pendingAvatarDownload && startAvatarDownload) {
|
||||
startAvatarDownload(conversation.id);
|
||||
startAvatarDownload(contact.id);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
conversation.id,
|
||||
contact.id,
|
||||
startAvatarDownload,
|
||||
enableClickToLoad,
|
||||
pendingAvatarDownload,
|
||||
@@ -85,9 +103,9 @@ export function AboutContactModal({
|
||||
const onVerifiedClick = useCallback(
|
||||
(ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
toggleSafetyNumberModal(conversation.id);
|
||||
toggleSafetyNumberModal(contact.id);
|
||||
},
|
||||
[toggleSafetyNumberModal, conversation.id]
|
||||
[toggleSafetyNumberModal, contact.id]
|
||||
);
|
||||
|
||||
const onProfileNameWarningClick = useCallback(
|
||||
@@ -99,31 +117,58 @@ export function AboutContactModal({
|
||||
);
|
||||
|
||||
let statusRow: React.JSX.Element | undefined;
|
||||
const hasLabel = contactNameColor && contactLabelString;
|
||||
const shouldShowLabel = isMe && hasLabel;
|
||||
const shouldShowAddLabel =
|
||||
isMe && !hasLabel && canAddLabel && isEditMemberLabelEnabled;
|
||||
const emojiLocalizer = useFunEmojiLocalizer();
|
||||
|
||||
let labelEmojiElement;
|
||||
if (
|
||||
shouldShowLabel &&
|
||||
contactLabelEmoji &&
|
||||
isEmojiVariantValue(contactLabelEmoji)
|
||||
) {
|
||||
const emojiKey = getEmojiVariantKeyByValue(contactLabelEmoji);
|
||||
const labelEmojiData = getEmojiVariantByKey(emojiKey);
|
||||
labelEmojiElement = (
|
||||
<>
|
||||
<FunStaticEmoji
|
||||
role="img"
|
||||
aria-label={emojiLocalizer.getLocaleShortName(labelEmojiData.key)}
|
||||
size={16}
|
||||
emoji={labelEmojiData}
|
||||
/>{' '}
|
||||
</>
|
||||
);
|
||||
} else if (shouldShowLabel && contactLabelEmoji) {
|
||||
labelEmojiElement = `${missingEmojiPlaceholder} `;
|
||||
}
|
||||
|
||||
if (isMe) {
|
||||
// No status for ourselves
|
||||
} else if (conversation.isBlocked) {
|
||||
} else if (contact.isBlocked) {
|
||||
statusRow = (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--blocked" />
|
||||
{i18n('icu:AboutContactModal__blocked', {
|
||||
name: conversation.title,
|
||||
name: contact.title,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
} else if (!conversation.acceptedMessageRequest) {
|
||||
} else if (!contact.acceptedMessageRequest) {
|
||||
statusRow = (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--message-request" />
|
||||
{i18n('icu:AboutContactModal__message-request')}
|
||||
</div>
|
||||
);
|
||||
} else if (!conversation.hasMessages && !conversation.profileSharing) {
|
||||
} else if (!contact.hasMessages && !contact.profileSharing) {
|
||||
statusRow = (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--no-dms" />
|
||||
{i18n('icu:AboutContactModal__no-dms', {
|
||||
name: conversation.title,
|
||||
name: contact.title,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
@@ -140,22 +185,21 @@ export function AboutContactModal({
|
||||
>
|
||||
<div className="AboutContactModal__row AboutContactModal__row--centered">
|
||||
<Avatar
|
||||
avatarPlaceholderGradient={conversation.avatarPlaceholderGradient}
|
||||
avatarUrl={conversation.avatarUrl}
|
||||
avatarPlaceholderGradient={contact.avatarPlaceholderGradient}
|
||||
avatarUrl={contact.avatarUrl}
|
||||
blur={avatarBlur}
|
||||
onClick={avatarOnClick}
|
||||
badge={undefined}
|
||||
color={conversation.color}
|
||||
color={contact.color}
|
||||
conversationType="direct"
|
||||
hasAvatar={conversation.hasAvatar}
|
||||
hasAvatar={contact.hasAvatar}
|
||||
i18n={i18n}
|
||||
loading={pendingAvatarDownload && !conversation.avatarUrl}
|
||||
profileName={conversation.profileName}
|
||||
loading={pendingAvatarDownload && !contact.avatarUrl}
|
||||
profileName={contact.profileName}
|
||||
size={AvatarSize.TWO_HUNDRED_SIXTEEN}
|
||||
title={conversation.title}
|
||||
title={contact.title}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="AboutContactModal__row">
|
||||
<h3 className="AboutContactModal__title">
|
||||
{isMe
|
||||
@@ -163,19 +207,18 @@ export function AboutContactModal({
|
||||
: i18n('icu:AboutContactModal__title')}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--profile" />
|
||||
|
||||
{canHaveNicknameAndNote(conversation) &&
|
||||
conversation.titleNoNickname !== conversation.title &&
|
||||
conversation.titleNoNickname ? (
|
||||
{canHaveNicknameAndNote(contact) &&
|
||||
contact.titleNoNickname !== contact.title &&
|
||||
contact.titleNoNickname ? (
|
||||
<span>
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:AboutContactModal__TitleAndTitleWithoutNickname"
|
||||
components={{
|
||||
nickname: <UserText text={conversation.title} />,
|
||||
nickname: <UserText text={contact.title} />,
|
||||
titleNoNickname: (
|
||||
<Tooltip
|
||||
className="AboutContactModal__TitleWithoutNickname__Tooltip"
|
||||
@@ -185,15 +228,13 @@ export function AboutContactModal({
|
||||
i18n={i18n}
|
||||
id="icu:AboutContactModal__TitleWithoutNickname__Tooltip"
|
||||
components={{
|
||||
title: (
|
||||
<UserText text={conversation.titleNoNickname} />
|
||||
),
|
||||
title: <UserText text={contact.titleNoNickname} />,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
delay={0}
|
||||
>
|
||||
<UserText text={conversation.titleNoNickname} />
|
||||
<UserText text={contact.titleNoNickname} />
|
||||
</Tooltip>
|
||||
),
|
||||
muted,
|
||||
@@ -201,14 +242,13 @@ export function AboutContactModal({
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<UserText text={conversation.title} />
|
||||
<UserText text={contact.title} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isMe && !fromOrAddedByTrustedContact ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i
|
||||
className={`AboutContactModal__row__icon AboutContactModal__row__icon--${conversation.type === 'group' ? 'group' : 'direct'}-question`}
|
||||
className={`AboutContactModal__row__icon AboutContactModal__row__icon--${contact.type === 'group' ? 'group' : 'direct'}-question`}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -222,7 +262,7 @@ export function AboutContactModal({
|
||||
}}
|
||||
i18n={i18n}
|
||||
id={
|
||||
conversation.type === 'group'
|
||||
contact.type === 'group'
|
||||
? 'icu:ConversationHero--group-names'
|
||||
: 'icu:ConversationHero--profile-names'
|
||||
}
|
||||
@@ -230,8 +270,7 @@ export function AboutContactModal({
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isMe && conversation.isVerified ? (
|
||||
{!isMe && contact.isVerified ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--verified" />
|
||||
<button
|
||||
@@ -243,17 +282,12 @@ export function AboutContactModal({
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isMe && conversation.about ? (
|
||||
{!isMe && contact.about ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--about" />
|
||||
<About
|
||||
className="AboutContactModal__about"
|
||||
text={conversation.about}
|
||||
/>
|
||||
<About className="AboutContactModal__about" text={contact.about} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isMe && isSignalConnection ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--connections" />
|
||||
@@ -266,23 +300,35 @@ export function AboutContactModal({
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isMe && isInSystemContacts(conversation) ? (
|
||||
{!isMe && isInSystemContacts(contact) ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--person" />
|
||||
{i18n('icu:AboutContactModal__system-contact', {
|
||||
name:
|
||||
conversation.systemGivenName ||
|
||||
conversation.firstName ||
|
||||
conversation.title,
|
||||
name: contact.systemGivenName || contact.firstName || contact.title,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{conversation.phoneNumber ? (
|
||||
{shouldShowLabel && (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--label" />
|
||||
<div className="AboutContactModal__label-container">
|
||||
{labelEmojiElement}
|
||||
{contactLabelString}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{shouldShowAddLabel && (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--label" />
|
||||
{i18n('icu:AboutContactModal__add-member-label')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contact.phoneNumber ? (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--phone" />
|
||||
<UserText text={conversation.phoneNumber} />
|
||||
<UserText text={contact.phoneNumber} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -294,8 +340,7 @@ export function AboutContactModal({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{conversation.note && (
|
||||
{contact.note && (
|
||||
<div className="AboutContactModal__row">
|
||||
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--note" />
|
||||
<button
|
||||
@@ -304,12 +349,11 @@ export function AboutContactModal({
|
||||
onClick={onOpenNotePreviewModal}
|
||||
>
|
||||
<div className="AboutContactModal__OneLineEllipsis">
|
||||
<UserText text={conversation.note} />
|
||||
<UserText text={contact.note} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{statusRow}
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -39,6 +39,9 @@ export default {
|
||||
badges: [],
|
||||
blockConversation: action('blockConversation'),
|
||||
contact: defaultContact,
|
||||
contactLabelEmoji: undefined,
|
||||
contactLabelString: undefined,
|
||||
contactNameColor: undefined,
|
||||
conversation: defaultGroup,
|
||||
hasActiveCall: false,
|
||||
hasStories: undefined,
|
||||
@@ -70,6 +73,29 @@ AsNonAdmin.args = {
|
||||
areWeAdmin: false,
|
||||
};
|
||||
|
||||
export const WithLabel = Template.bind({});
|
||||
WithLabel.args = {
|
||||
areWeAdmin: false,
|
||||
contactLabelEmoji: '💪🏼',
|
||||
contactLabelString: 'Strong',
|
||||
contactNameColor: '180',
|
||||
};
|
||||
|
||||
export const WithLabelNoEmoji = Template.bind({});
|
||||
WithLabelNoEmoji.args = {
|
||||
areWeAdmin: false,
|
||||
contactLabelString: 'Strong',
|
||||
contactNameColor: '220',
|
||||
};
|
||||
|
||||
export const WithLabelInvalidEmoji = Template.bind({});
|
||||
WithLabelInvalidEmoji.args = {
|
||||
areWeAdmin: false,
|
||||
contactLabelEmoji: '%',
|
||||
contactLabelString: 'Strong',
|
||||
contactNameColor: '220',
|
||||
};
|
||||
|
||||
export const AsAdmin = Template.bind({});
|
||||
AsAdmin.args = {
|
||||
areWeAdmin: true,
|
||||
@@ -97,14 +123,6 @@ WithoutPhoneNumber.args = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ViewingSelf = Template.bind({});
|
||||
ViewingSelf.args = {
|
||||
contact: {
|
||||
...defaultContact,
|
||||
isMe: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithBadges = Template.bind({});
|
||||
WithBadges.args = {
|
||||
badges: getFakeBadges(2),
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
InAnotherCallTooltip,
|
||||
getTooltipContent,
|
||||
} from './InAnotherCallTooltip.dom.js';
|
||||
import type { ContactModalStateType } from '../../state/ducks/globalModals.preload.js';
|
||||
import { GroupMemberLabel } from './ContactName.dom.js';
|
||||
|
||||
const log = createLogger('ContactModal');
|
||||
|
||||
@@ -39,6 +41,9 @@ export type PropsDataType = {
|
||||
areWeAdmin: boolean;
|
||||
badges: ReadonlyArray<BadgeType>;
|
||||
contact?: ConversationType;
|
||||
contactLabelEmoji: string | undefined;
|
||||
contactLabelString: string | undefined;
|
||||
contactNameColor: string | undefined;
|
||||
conversation?: ConversationType;
|
||||
hasStories?: HasStories;
|
||||
readonly i18n: LocalizerType;
|
||||
@@ -59,7 +64,7 @@ type PropsActionType = {
|
||||
showConversation: ShowConversationType;
|
||||
startAvatarDownload: () => void;
|
||||
toggleAdmin: (conversationId: string, contactId: string) => void;
|
||||
toggleAboutContactModal: (conversationId: string) => unknown;
|
||||
toggleAboutContactModal: (options: ContactModalStateType) => unknown;
|
||||
togglePip: () => void;
|
||||
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
||||
toggleAddUserToAnotherGroupModal: (conversationId: string) => void;
|
||||
@@ -87,6 +92,9 @@ export function ContactModal({
|
||||
badges,
|
||||
blockConversation,
|
||||
contact,
|
||||
contactLabelEmoji,
|
||||
contactLabelString,
|
||||
contactNameColor,
|
||||
conversation,
|
||||
hasActiveCall,
|
||||
hasStories,
|
||||
@@ -344,7 +352,7 @@ export function ContactModal({
|
||||
className="ContactModal__name"
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
toggleAboutContactModal(contact.id);
|
||||
toggleAboutContactModal({ contactId: contact.id });
|
||||
}}
|
||||
>
|
||||
<div className="ContactModal__name__text">
|
||||
@@ -361,6 +369,19 @@ export function ContactModal({
|
||||
</div>
|
||||
<i className="ContactModal__name__chevron" />
|
||||
</button>
|
||||
{contactLabelString && contactNameColor && (
|
||||
<div className="ContactModal__member-label">
|
||||
<GroupMemberLabel
|
||||
emojiSize={14}
|
||||
contactLabel={{
|
||||
labelEmoji: contactLabelEmoji,
|
||||
labelString: contactLabelString,
|
||||
}}
|
||||
contactNameColor={contactNameColor}
|
||||
context="list"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!contact.isMe && renderQuickActions(contact.id)}
|
||||
<div className="ContactModal__divider" />
|
||||
<div className="ContactModal__button-container">
|
||||
|
||||
@@ -36,3 +36,57 @@ export function Colors(): React.JSX.Element {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ColorsWithLabels(): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{ContactNameColors.map(color => (
|
||||
<div key={color}>
|
||||
<ContactName
|
||||
title={`Hello ${color}`}
|
||||
contactNameColor={color}
|
||||
contactLabel={{ labelEmoji: '✅', labelString: 'Task Wrangler' }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ColorsWithNoLabelEmoji(): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{ContactNameColors.map(color => (
|
||||
<div key={color}>
|
||||
<ContactName
|
||||
title={`Hello ${color}`}
|
||||
contactNameColor={color}
|
||||
contactLabel={{
|
||||
labelEmoji: undefined,
|
||||
labelString: 'Task Wrangler',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ColorsWithInvalidLabelEmoji(): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{ContactNameColors.map(color => (
|
||||
<div key={color}>
|
||||
<ContactName
|
||||
title={`Hello ${color}`}
|
||||
contactNameColor={color}
|
||||
contactLabel={{
|
||||
labelEmoji: '&',
|
||||
labelString: 'Task Wrangler',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,28 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { Emojify } from './Emojify.dom.js';
|
||||
import type { ContactNameColorType } from '../../types/Colors.std.js';
|
||||
import { getClassNamesFor } from '../../util/getClassNamesFor.std.js';
|
||||
import type { ConversationType } from '../../state/ducks/conversations.preload.js';
|
||||
import { isSignalConversation as getIsSignalConversation } from '../../util/isSignalConversation.dom.js';
|
||||
import {
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from '../fun/data/emojis.std.js';
|
||||
import { useFunEmojiLocalizer } from '../fun/useFunEmojiLocalizer.dom.js';
|
||||
import { FunStaticEmoji } from '../fun/FunEmoji.dom.js';
|
||||
|
||||
import type { ConversationType } from '../../state/ducks/conversations.preload.js';
|
||||
import type { ContactNameColorType } from '../../types/Colors.std.js';
|
||||
import type { FunStaticEmojiSize } from '../fun/FunEmoji.dom.js';
|
||||
|
||||
export const missingEmojiPlaceholder = '⍰';
|
||||
|
||||
export type ContactNameData = {
|
||||
contactNameColor?: ContactNameColorType;
|
||||
contactLabel?: { labelString: string; labelEmoji: string | undefined };
|
||||
firstName?: string;
|
||||
isSignalConversation?: boolean;
|
||||
isMe?: boolean;
|
||||
@@ -47,6 +61,7 @@ export type PropsType = ContactNameData & {
|
||||
};
|
||||
|
||||
export function ContactName({
|
||||
contactLabel,
|
||||
contactNameColor,
|
||||
firstName,
|
||||
isSignalConversation,
|
||||
@@ -85,6 +100,96 @@ export function ContactName({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{contactLabel && (
|
||||
<>
|
||||
{' '}
|
||||
<GroupMemberLabel
|
||||
contactLabel={contactLabel}
|
||||
context="bubble"
|
||||
contactNameColor={contactNameColor}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</WrappingElement>
|
||||
);
|
||||
}
|
||||
|
||||
export type Context = 'bubble' | 'list';
|
||||
|
||||
export function GroupMemberLabel({
|
||||
emojiSize = 12,
|
||||
contactLabel,
|
||||
contactNameColor,
|
||||
context,
|
||||
module,
|
||||
}: {
|
||||
emojiSize?: FunStaticEmojiSize;
|
||||
contactLabel?: { labelString: string; labelEmoji: string | undefined };
|
||||
contactNameColor?: ContactNameColorType;
|
||||
context: Context;
|
||||
module?: string;
|
||||
}): ReactNode {
|
||||
const emojiLocalizer = useFunEmojiLocalizer();
|
||||
const getClassName = getClassNamesFor('module-contact-name', module);
|
||||
|
||||
if (!contactLabel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { labelEmoji, labelString } = contactLabel;
|
||||
|
||||
let emojiElement;
|
||||
if (labelEmoji && isEmojiVariantValue(labelEmoji)) {
|
||||
const emojiKey = getEmojiVariantKeyByValue(labelEmoji);
|
||||
const emojiData = getEmojiVariantByKey(emojiKey);
|
||||
|
||||
emojiElement = (
|
||||
<span
|
||||
className={classNames(
|
||||
getClassName('--label-pill--emoji'),
|
||||
getClassName(`--label-pill--${context}--emoji`)
|
||||
)}
|
||||
>
|
||||
<FunStaticEmoji
|
||||
role="img"
|
||||
aria-label={emojiLocalizer.getLocaleShortName(emojiData.key)}
|
||||
size={emojiSize}
|
||||
emoji={emojiData}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else if (labelEmoji) {
|
||||
emojiElement = (
|
||||
<span
|
||||
className={classNames(
|
||||
getClassName('--label-pill--emoji'),
|
||||
getClassName(`--label-pill--${context}--emoji`)
|
||||
)}
|
||||
>
|
||||
{missingEmojiPlaceholder}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
getClassName('--label-pill'),
|
||||
getClassName(`--label-pill--${context}`),
|
||||
getClassName(`--${contactNameColor}--label-pill--${context}`)
|
||||
)}
|
||||
>
|
||||
<span className={getClassName('--label-pill--inner')}>
|
||||
{emojiElement}
|
||||
<span
|
||||
className={classNames(
|
||||
getClassName('--label-pill--text'),
|
||||
getClassName(`--label-pill--${context}--text`)
|
||||
)}
|
||||
>
|
||||
<Emojify text={labelString} />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ const createMemberships = ({
|
||||
return Array.from(new Array(count)).map(
|
||||
(_, i): GroupV2Membership => ({
|
||||
isAdmin: i % 3 === 0,
|
||||
labelEmoji: i % 6 === 0 ? '🟢' : undefined,
|
||||
labelString: i % 3 === 0 ? `Task Wrangler ${i}` : undefined,
|
||||
member: unknownContactIndices.includes(i)
|
||||
? getDefaultConversation({
|
||||
isMe: includeMe && i === 0,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { StoryViewModeType } from '../../types/Stories.std.js';
|
||||
import { Button, ButtonVariant } from '../Button.dom.js';
|
||||
import { SafetyTipsModal } from '../SafetyTipsModal.dom.js';
|
||||
import { I18n } from '../I18n.dom.js';
|
||||
import type { ContactModalStateType } from '../../state/ducks/globalModals.preload.js';
|
||||
|
||||
export type Props = {
|
||||
about?: string;
|
||||
@@ -41,7 +42,7 @@ export type Props = {
|
||||
startAvatarDownload: () => void;
|
||||
theme: ThemeType;
|
||||
viewUserStories: ViewUserStoriesActionCreatorType;
|
||||
toggleAboutContactModal: (conversationId: string) => unknown;
|
||||
toggleAboutContactModal: (options: ContactModalStateType) => unknown;
|
||||
toggleProfileNameWarningModal: (conversationType?: string) => unknown;
|
||||
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
|
||||
|
||||
@@ -306,7 +307,7 @@ export function ConversationHero({
|
||||
className="module-conversation-hero__title"
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
toggleAboutContactModal(id);
|
||||
toggleAboutContactModal({ contactId: id });
|
||||
}}
|
||||
>
|
||||
<ContactName title={title} />
|
||||
|
||||
@@ -247,6 +247,7 @@ export type PropsData = {
|
||||
id: string;
|
||||
renderingContext: RenderingContextType;
|
||||
contactNameColor?: ContactNameColorType;
|
||||
contactLabel?: { labelString: string; labelEmoji: string | undefined };
|
||||
conversationColor: ConversationColorType;
|
||||
conversationTitle: string;
|
||||
customColor?: CustomColorType;
|
||||
@@ -1135,7 +1136,8 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
#renderAuthor(): ReactNode {
|
||||
const { author, contactNameColor, i18n, isSticker, quote } = this.props;
|
||||
const { author, contactLabel, contactNameColor, i18n, isSticker, quote } =
|
||||
this.props;
|
||||
|
||||
if (!this.#shouldRenderAuthor()) {
|
||||
return null;
|
||||
@@ -1153,6 +1155,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
>
|
||||
<ContactName
|
||||
contactNameColor={contactNameColor}
|
||||
contactLabel={contactLabel}
|
||||
title={author.isMe ? i18n('icu:you') : author.title}
|
||||
module={moduleName}
|
||||
/>
|
||||
@@ -3244,6 +3247,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||
: null,
|
||||
isTargeted ? 'module-message__container--targeted' : null,
|
||||
lighterSelect ? 'module-message__container--targeted-lighter' : null,
|
||||
isStickerLike ? 'module-message__container--sticker-like' : null,
|
||||
!isStickerLike ? `module-message__container--${direction}` : null,
|
||||
isEmojiOnly ? 'module-message__container--emoji' : null,
|
||||
!isStickerLike && direction === 'outgoing'
|
||||
|
||||
@@ -256,6 +256,8 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
conversationId: overrideProps.conversationId ?? '',
|
||||
conversationType: overrideProps.conversationType || 'direct',
|
||||
contact: overrideProps.contact,
|
||||
contactNameColor: overrideProps.contactNameColor,
|
||||
contactLabel: overrideProps.contactLabel,
|
||||
// disableMenu: overrideProps.disableMenu,
|
||||
deletedForEveryone: overrideProps.deletedForEveryone,
|
||||
disableScroll: overrideProps.disableScroll,
|
||||
@@ -370,7 +372,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
});
|
||||
|
||||
const renderMany = (propsArray: ReadonlyArray<Props>) => (
|
||||
<>
|
||||
<div className="module-timeline--width-wide">
|
||||
{propsArray.map((message, index) => (
|
||||
<TimelineMessage
|
||||
key={`${message.text}_${index}_${message.direction}`}
|
||||
@@ -379,7 +381,7 @@ const renderMany = (propsArray: ReadonlyArray<Props>) => (
|
||||
shouldCollapseBelow={Boolean(propsArray[index + 1])}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderThree = (props: Props) =>
|
||||
@@ -402,7 +404,7 @@ const renderBothDirections = (props: Props) => (
|
||||
);
|
||||
|
||||
const renderOneInBothDirections = (props: Props) => (
|
||||
<>
|
||||
<div className="module-timeline--width-wide">
|
||||
<TimelineMessage {...props} />
|
||||
<TimelineMessage
|
||||
{...props}
|
||||
@@ -412,7 +414,7 @@ const renderOneInBothDirections = (props: Props) => (
|
||||
canEndPoll: true,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const PlainMessage = Template.bind({});
|
||||
@@ -757,6 +759,7 @@ export const AvatarInGroup = Template.bind({});
|
||||
AvatarInGroup.args = {
|
||||
author: getDefaultConversation({ avatarUrl: pngUrl }),
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
status: 'sent',
|
||||
text: 'Hello it is me, the saxophone.',
|
||||
};
|
||||
@@ -765,10 +768,55 @@ export const BadgeInGroup = Template.bind({});
|
||||
BadgeInGroup.args = {
|
||||
conversationType: 'group',
|
||||
getPreferredBadge: () => getFakeBadge(),
|
||||
contactNameColor: '300',
|
||||
status: 'sent',
|
||||
text: 'Hello it is me, the saxophone.',
|
||||
};
|
||||
|
||||
export const LabelInGroup = Template.bind({});
|
||||
LabelInGroup.args = {
|
||||
conversationType: 'group',
|
||||
status: 'sent',
|
||||
text: 'Hello it is me, the saxophone.',
|
||||
contactNameColor: '260',
|
||||
contactLabel: {
|
||||
labelEmoji: '🍗',
|
||||
labelString: 'Chicken Taster',
|
||||
},
|
||||
};
|
||||
|
||||
export const LabelInGroupWithLongName = Template.bind({});
|
||||
LabelInGroupWithLongName.args = {
|
||||
conversationType: 'group',
|
||||
status: 'sent',
|
||||
text: 'Hello it is me, the saxophone.',
|
||||
author: {
|
||||
...getDefaultConversation(),
|
||||
title: 'Long long long long long long long long long long long name',
|
||||
},
|
||||
contactNameColor: '260',
|
||||
contactLabel: {
|
||||
labelEmoji: '🍗',
|
||||
labelString: 'Chicken Taster',
|
||||
},
|
||||
};
|
||||
|
||||
export const LabelInGroupWithLongNameAndLongMessage = Template.bind({});
|
||||
LabelInGroupWithLongNameAndLongMessage.args = {
|
||||
conversationType: 'group',
|
||||
status: 'sent',
|
||||
text: 'Hello it is me, the saxophone. I am a good friend of yours. Do you remember? A long long long long long long long time ago.',
|
||||
author: {
|
||||
...getDefaultConversation(),
|
||||
title: 'Long long long long long long long long long long long name',
|
||||
},
|
||||
contactNameColor: '260',
|
||||
contactLabel: {
|
||||
labelEmoji: '🍗',
|
||||
labelString: 'Chicken Taster',
|
||||
},
|
||||
};
|
||||
|
||||
export const Sticker = Template.bind({});
|
||||
Sticker.args = {
|
||||
attachments: [
|
||||
@@ -784,6 +832,69 @@ Sticker.args = {
|
||||
status: 'sent',
|
||||
};
|
||||
|
||||
export const StickerInGroup = Template.bind({});
|
||||
StickerInGroup.args = {
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
url: '/fixtures/512x515-thumbs-up-lincoln.webp',
|
||||
fileName: '512x515-thumbs-up-lincoln.webp',
|
||||
contentType: IMAGE_WEBP,
|
||||
width: 128,
|
||||
height: 128,
|
||||
}),
|
||||
],
|
||||
conversationType: 'group',
|
||||
contactNameColor: '180',
|
||||
isSticker: true,
|
||||
status: 'sent',
|
||||
};
|
||||
|
||||
export const StickerWithLabelInGroup = Template.bind({});
|
||||
StickerWithLabelInGroup.args = {
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
url: '/fixtures/512x515-thumbs-up-lincoln.webp',
|
||||
fileName: '512x515-thumbs-up-lincoln.webp',
|
||||
contentType: IMAGE_WEBP,
|
||||
width: 128,
|
||||
height: 128,
|
||||
}),
|
||||
],
|
||||
conversationType: 'group',
|
||||
isSticker: true,
|
||||
status: 'sent',
|
||||
contactNameColor: '260',
|
||||
contactLabel: {
|
||||
labelEmoji: '🍗',
|
||||
labelString: 'Chicken Taster',
|
||||
},
|
||||
};
|
||||
|
||||
export const StickerWithLongNameAndLabelInGroup = Template.bind({});
|
||||
StickerWithLongNameAndLabelInGroup.args = {
|
||||
attachments: [
|
||||
fakeAttachment({
|
||||
url: '/fixtures/512x515-thumbs-up-lincoln.webp',
|
||||
fileName: '512x515-thumbs-up-lincoln.webp',
|
||||
contentType: IMAGE_WEBP,
|
||||
width: 128,
|
||||
height: 128,
|
||||
}),
|
||||
],
|
||||
conversationType: 'group',
|
||||
isSticker: true,
|
||||
status: 'sent',
|
||||
author: {
|
||||
...getDefaultConversation(),
|
||||
title: 'Long long long long long long long long long long long name',
|
||||
},
|
||||
contactNameColor: '280',
|
||||
contactLabel: {
|
||||
labelEmoji: '🍗',
|
||||
labelString: 'Chicken Taster',
|
||||
},
|
||||
};
|
||||
|
||||
export const Quote = Template.bind({});
|
||||
Quote.args = {
|
||||
quote: {
|
||||
@@ -806,6 +917,7 @@ Quote.args = {
|
||||
badges: [],
|
||||
},
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
};
|
||||
|
||||
export function Deleted(): React.JSX.Element {
|
||||
@@ -834,6 +946,7 @@ export const DeletedWithExpireTimer = Template.bind({});
|
||||
DeletedWithExpireTimer.args = {
|
||||
timestamp: Date.now() - 60 * 1000,
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
deletedForEveryone: true,
|
||||
canForward: false,
|
||||
expirationLength: 5 * 60 * 1000,
|
||||
@@ -846,6 +959,7 @@ export function DeletedWithError(): React.JSX.Element {
|
||||
timestamp: Date.now() - 60 * 1000,
|
||||
// canDeleteForEveryone: true,
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
deletedForEveryone: true,
|
||||
status: 'partial-sent',
|
||||
direction: 'outgoing',
|
||||
@@ -854,6 +968,7 @@ export function DeletedWithError(): React.JSX.Element {
|
||||
timestamp: Date.now() - 60 * 1000,
|
||||
// canDeleteForEveryone: true,
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
deletedForEveryone: true,
|
||||
status: 'error',
|
||||
direction: 'outgoing',
|
||||
@@ -985,6 +1100,7 @@ LinkPreviewInGroup.args = {
|
||||
status: 'sent',
|
||||
text: 'Be sure to look at https://www.signal.org',
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
};
|
||||
|
||||
export const LinkPreviewWithLongWord = Template.bind({});
|
||||
@@ -1010,6 +1126,7 @@ LinkPreviewWithLongWord.args = {
|
||||
status: 'sent',
|
||||
text: 'Be sure to look at https://www.signal.org',
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
};
|
||||
|
||||
export const LinkPreviewWithQuote = Template.bind({});
|
||||
@@ -1048,6 +1165,7 @@ LinkPreviewWithQuote.args = {
|
||||
status: 'sent',
|
||||
text: 'Be sure to look at https://www.signal.org',
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
};
|
||||
|
||||
export const LinkPreviewWithSmallImage = Template.bind({});
|
||||
@@ -1851,6 +1969,7 @@ GifInAGroup.args = {
|
||||
}),
|
||||
],
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
status: 'sent',
|
||||
};
|
||||
|
||||
@@ -2168,6 +2287,7 @@ function createMockPollWithVoters(
|
||||
export const Poll = Template.bind({});
|
||||
Poll.args = {
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
poll: {
|
||||
question: 'What should we have for lunch?',
|
||||
options: ['Pizza 🍕', 'Sushi 🍱', 'Tacos 🌮', 'Salad 🥗'],
|
||||
@@ -2182,6 +2302,7 @@ Poll.args = {
|
||||
export const PollMultipleChoice = Template.bind({});
|
||||
PollMultipleChoice.args = {
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
poll: {
|
||||
question: 'Which features would you like to see in the next update?',
|
||||
options: ['Dark mode', 'Video calls', 'File sharing', 'Reactions', 'Polls'],
|
||||
@@ -2196,6 +2317,7 @@ PollMultipleChoice.args = {
|
||||
export const PollWithVotes = Template.bind({});
|
||||
PollWithVotes.args = {
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
poll: createMockPollWithVoters(
|
||||
'Best day for the team meeting?',
|
||||
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
|
||||
@@ -2220,6 +2342,7 @@ PollWithVotes.args = {
|
||||
export const PollWithPendingVotes = Template.bind({});
|
||||
PollWithPendingVotes.args = {
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
poll: createMockPollWithVoters(
|
||||
'Best day for the team meeting?',
|
||||
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
|
||||
@@ -2248,6 +2371,7 @@ PollWithPendingVotes.args = {
|
||||
export const PollTerminated = Template.bind({});
|
||||
PollTerminated.args = {
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
poll: createMockPollWithVoters(
|
||||
'Quick poll: Coffee or tea?',
|
||||
['Coffee ☕', 'Tea 🍵'],
|
||||
@@ -2274,6 +2398,7 @@ PollTerminated.args = {
|
||||
export const PollLongText = Template.bind({});
|
||||
PollLongText.args = {
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
poll: createMockPollWithVoters(
|
||||
'Given the current situation with remote work becoming more prevalent, what would be your preferred working arrangement for the future once everything stabilizes?',
|
||||
[
|
||||
@@ -2299,6 +2424,7 @@ PollLongText.args = {
|
||||
export const PollMultipleChoiceWithVotes = Template.bind({});
|
||||
PollMultipleChoiceWithVotes.args = {
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
poll: createMockPollWithVoters(
|
||||
'Which toppings do you want on the pizza?',
|
||||
[
|
||||
@@ -2414,6 +2540,7 @@ export function PollAnimationPlayground(): React.JSX.Element {
|
||||
|
||||
const props = createProps({
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
poll,
|
||||
status: 'sent',
|
||||
sendPollVote: handleSendPollVote,
|
||||
@@ -2660,6 +2787,7 @@ TapToViewImageInGroup.args = {
|
||||
isTapToView: true,
|
||||
status: 'sent',
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
};
|
||||
|
||||
export const TapToViewVideo = Template.bind({});
|
||||
@@ -2754,7 +2882,7 @@ export function Colors(): React.JSX.Element {
|
||||
<>
|
||||
{ConversationColors.map(color => (
|
||||
<div key={color}>
|
||||
{renderBothDirections(
|
||||
{renderOneInBothDirections(
|
||||
createProps({
|
||||
conversationColor: color,
|
||||
text: `Here is a preview of the chat color: ${color}. The color is visible to only you.`,
|
||||
@@ -2897,18 +3025,21 @@ export const CollapsingTextOnlyGroupMessages = (): React.JSX.Element => {
|
||||
createProps({
|
||||
author,
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
text: 'One',
|
||||
timestamp: Date.now() - 2 * MINUTE,
|
||||
}),
|
||||
createProps({
|
||||
author,
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
text: 'Two',
|
||||
timestamp: Date.now() - MINUTE,
|
||||
}),
|
||||
createProps({
|
||||
author,
|
||||
conversationType: 'group',
|
||||
contactNameColor: '100',
|
||||
text: 'Three',
|
||||
}),
|
||||
]);
|
||||
|
||||
+126
-87
@@ -18,6 +18,8 @@ import { ThemeType } from '../../../types/Util.std.js';
|
||||
import { DurationInSeconds } from '../../../util/durations/index.std.js';
|
||||
import { NavTab } from '../../../types/Nav.std.js';
|
||||
import { getFakeCallHistoryGroup } from '../../../test-helpers/getFakeCallHistoryGroup.std.js';
|
||||
import { ContactNameColors } from '../../../types/Colors.std.js';
|
||||
import { isNotNil } from '../../../util/isNotNil.std.js';
|
||||
|
||||
const { times } = lodash;
|
||||
|
||||
@@ -41,97 +43,118 @@ const allCandidateContacts = times(10, () => getDefaultConversation());
|
||||
const createProps = (
|
||||
hasGroupLink = false,
|
||||
expireTimer?: DurationInSeconds
|
||||
): Props => ({
|
||||
acceptConversation: action('acceptConversation'),
|
||||
addMembersToGroup: async () => {
|
||||
action('addMembersToGroup');
|
||||
},
|
||||
areWeASubscriber: false,
|
||||
blockConversation: action('blockConversation'),
|
||||
canEditGroupInfo: false,
|
||||
canAddNewMembers: false,
|
||||
conversation: expireTimer
|
||||
? {
|
||||
...conversation,
|
||||
expireTimer,
|
||||
}
|
||||
: conversation,
|
||||
hasActiveCall: false,
|
||||
hasGroupLink,
|
||||
getPreferredBadge: () => undefined,
|
||||
getProfilesForConversation: action('getProfilesForConversation'),
|
||||
groupsInCommon: [],
|
||||
i18n,
|
||||
isAdmin: false,
|
||||
isGroup: true,
|
||||
isSignalConversation: false,
|
||||
leaveGroup: action('leaveGroup'),
|
||||
hasMedia: true,
|
||||
memberships: times(32, i => ({
|
||||
): Props => {
|
||||
const memberships = times(32, i => ({
|
||||
isAdmin: i === 1,
|
||||
labelEmoji: i % 6 === 0 ? '🟢' : undefined,
|
||||
labelString: i % 3 === 0 ? `Task Wrangler ${i}` : undefined,
|
||||
member: getDefaultConversation({
|
||||
isMe: i === 2,
|
||||
}),
|
||||
})),
|
||||
maxGroupSize: 1001,
|
||||
maxRecommendedGroupSize: 151,
|
||||
pendingApprovalMemberships: times(8, () => ({
|
||||
member: getDefaultConversation(),
|
||||
})),
|
||||
pendingMemberships: times(5, () => ({
|
||||
metadata: {},
|
||||
member: getDefaultConversation(),
|
||||
})),
|
||||
selectedNavTab: NavTab.Chats,
|
||||
setDisappearingMessages: action('setDisappearingMessages'),
|
||||
showContactModal: action('showContactModal'),
|
||||
pushPanelForConversation: action('pushPanelForConversation'),
|
||||
showConversation: action('showConversation'),
|
||||
startAvatarDownload: action('startAvatarDownload'),
|
||||
updateGroupAttributes: async () => {
|
||||
action('updateGroupAttributes')();
|
||||
},
|
||||
deleteAvatarFromDisk: action('deleteAvatarFromDisk'),
|
||||
replaceAvatar: action('replaceAvatar'),
|
||||
saveAvatarToDisk: action('saveAvatarToDisk'),
|
||||
setMuteExpiration: action('setMuteExpiration'),
|
||||
userAvatarData: [],
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
toggleAboutContactModal: action('toggleAboutContactModal'),
|
||||
toggleAddUserToAnotherGroupModal: action('toggleAddUserToAnotherGroup'),
|
||||
onDeleteNicknameAndNote: action('onDeleteNicknameAndNote'),
|
||||
onOpenEditNicknameAndNoteModal: action('onOpenEditNicknameAndNoteModal'),
|
||||
onOutgoingAudioCallInConversation: action(
|
||||
'onOutgoingAudioCallInConversation'
|
||||
),
|
||||
onOutgoingVideoCallInConversation: action(
|
||||
'onOutgoingVideoCallInConversation'
|
||||
),
|
||||
searchInConversation: action('searchInConversation'),
|
||||
theme: ThemeType.light,
|
||||
renderChooseGroupMembersModal: props => {
|
||||
return (
|
||||
<ChooseGroupMembersModal
|
||||
{...props}
|
||||
candidateContacts={allCandidateContacts}
|
||||
selectedContacts={[]}
|
||||
regionCode="US"
|
||||
theme={ThemeType.light}
|
||||
i18n={i18n}
|
||||
lookupConversationWithoutServiceId={makeFakeLookupConversationWithoutServiceId()}
|
||||
ourE164={undefined}
|
||||
ourUsername={undefined}
|
||||
showUserNotFoundModal={action('showUserNotFoundModal')}
|
||||
username={undefined}
|
||||
/>
|
||||
);
|
||||
},
|
||||
renderConfirmAdditionsModal: props => {
|
||||
return (
|
||||
<ConfirmAdditionsModal {...props} selectedContacts={[]} i18n={i18n} />
|
||||
);
|
||||
},
|
||||
});
|
||||
}));
|
||||
const memberColors = new Map<string, string>(
|
||||
memberships
|
||||
.map((membership, i): [string, string] | null => {
|
||||
const { serviceId } = membership.member;
|
||||
|
||||
if (!serviceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [serviceId.toString(), ContactNameColors[i]];
|
||||
})
|
||||
.filter(isNotNil)
|
||||
);
|
||||
|
||||
return {
|
||||
acceptConversation: action('acceptConversation'),
|
||||
addMembersToGroup: async () => {
|
||||
action('addMembersToGroup');
|
||||
},
|
||||
areWeASubscriber: false,
|
||||
blockConversation: action('blockConversation'),
|
||||
canEditGroupInfo: false,
|
||||
canAddNewMembers: false,
|
||||
conversation: expireTimer
|
||||
? {
|
||||
...conversation,
|
||||
expireTimer,
|
||||
}
|
||||
: conversation,
|
||||
hasActiveCall: false,
|
||||
hasGroupLink,
|
||||
getPreferredBadge: () => undefined,
|
||||
getProfilesForConversation: action('getProfilesForConversation'),
|
||||
groupsInCommon: [],
|
||||
i18n,
|
||||
isAdmin: false,
|
||||
isEditMemberLabelEnabled: true,
|
||||
isGroup: true,
|
||||
isSignalConversation: false,
|
||||
leaveGroup: action('leaveGroup'),
|
||||
hasMedia: true,
|
||||
memberships,
|
||||
memberColors,
|
||||
maxGroupSize: 1001,
|
||||
maxRecommendedGroupSize: 151,
|
||||
pendingApprovalMemberships: times(8, () => ({
|
||||
member: getDefaultConversation(),
|
||||
})),
|
||||
pendingMemberships: times(5, () => ({
|
||||
metadata: {},
|
||||
member: getDefaultConversation(),
|
||||
})),
|
||||
selectedNavTab: NavTab.Chats,
|
||||
setDisappearingMessages: action('setDisappearingMessages'),
|
||||
showContactModal: action('showContactModal'),
|
||||
pushPanelForConversation: action('pushPanelForConversation'),
|
||||
showConversation: action('showConversation'),
|
||||
startAvatarDownload: action('startAvatarDownload'),
|
||||
updateGroupAttributes: async () => {
|
||||
action('updateGroupAttributes')();
|
||||
},
|
||||
deleteAvatarFromDisk: action('deleteAvatarFromDisk'),
|
||||
replaceAvatar: action('replaceAvatar'),
|
||||
saveAvatarToDisk: action('saveAvatarToDisk'),
|
||||
setMuteExpiration: action('setMuteExpiration'),
|
||||
userAvatarData: [],
|
||||
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
|
||||
toggleAboutContactModal: action('toggleAboutContactModal'),
|
||||
toggleAddUserToAnotherGroupModal: action('toggleAddUserToAnotherGroup'),
|
||||
onDeleteNicknameAndNote: action('onDeleteNicknameAndNote'),
|
||||
onOpenEditNicknameAndNoteModal: action('onOpenEditNicknameAndNoteModal'),
|
||||
onOutgoingAudioCallInConversation: action(
|
||||
'onOutgoingAudioCallInConversation'
|
||||
),
|
||||
onOutgoingVideoCallInConversation: action(
|
||||
'onOutgoingVideoCallInConversation'
|
||||
),
|
||||
searchInConversation: action('searchInConversation'),
|
||||
theme: ThemeType.light,
|
||||
renderChooseGroupMembersModal: props => {
|
||||
return (
|
||||
<ChooseGroupMembersModal
|
||||
{...props}
|
||||
candidateContacts={allCandidateContacts}
|
||||
selectedContacts={[]}
|
||||
regionCode="US"
|
||||
theme={ThemeType.light}
|
||||
i18n={i18n}
|
||||
lookupConversationWithoutServiceId={makeFakeLookupConversationWithoutServiceId()}
|
||||
ourE164={undefined}
|
||||
ourUsername={undefined}
|
||||
showUserNotFoundModal={action('showUserNotFoundModal')}
|
||||
username={undefined}
|
||||
/>
|
||||
);
|
||||
},
|
||||
renderConfirmAdditionsModal: props => {
|
||||
return (
|
||||
<ConfirmAdditionsModal {...props} selectedContacts={[]} i18n={i18n} />
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function Basic(): React.JSX.Element {
|
||||
const props = createProps();
|
||||
@@ -170,6 +193,8 @@ export function AsLastAdmin(): React.JSX.Element {
|
||||
isAdmin
|
||||
memberships={times(32, i => ({
|
||||
isAdmin: i === 2,
|
||||
labelEmoji: i % 6 === 0 ? '🟢' : undefined,
|
||||
labelString: i % 3 === 0 ? `Last Admin ${i}` : undefined,
|
||||
member: getDefaultConversation({
|
||||
isMe: i === 2,
|
||||
}),
|
||||
@@ -188,6 +213,8 @@ export function AsOnlyAdmin(): React.JSX.Element {
|
||||
memberships={[
|
||||
{
|
||||
isAdmin: true,
|
||||
labelEmoji: undefined,
|
||||
labelString: undefined,
|
||||
member: getDefaultConversation({
|
||||
isMe: true,
|
||||
}),
|
||||
@@ -203,6 +230,18 @@ export function GroupEditable(): React.JSX.Element {
|
||||
return <ConversationDetails {...props} canEditGroupInfo />;
|
||||
}
|
||||
|
||||
export function GroupEditableEditLabelDisabled(): React.JSX.Element {
|
||||
const props = createProps();
|
||||
|
||||
return (
|
||||
<ConversationDetails
|
||||
{...props}
|
||||
canEditGroupInfo
|
||||
isEditMemberLabelEnabled={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GroupEditableWithCustomDisappearingTimeout(): React.JSX.Element {
|
||||
const props = createProps(false, DurationInSeconds.fromDays(3));
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
ConversationType,
|
||||
PushPanelForConversationActionType,
|
||||
ShowConversationType,
|
||||
UpdateGroupAttributesType,
|
||||
} from '../../../state/ducks/conversations.preload.js';
|
||||
import type { PreferredBadgeSelectorType } from '../../../state/selectors/badges.preload.js';
|
||||
import type { SmartChooseGroupMembersModalPropsType } from '../../../state/smart/ChooseGroupMembersModal.preload.js';
|
||||
@@ -63,6 +64,7 @@ import {
|
||||
getTooltipContent,
|
||||
} from '../InAnotherCallTooltip.dom.js';
|
||||
import { BadgeSustainerInstructionsDialog } from '../../BadgeSustainerInstructionsDialog.dom.js';
|
||||
import type { ContactModalStateType } from '../../../state/ducks/globalModals.preload.js';
|
||||
|
||||
enum ModalState {
|
||||
AddingGroupMembers,
|
||||
@@ -88,12 +90,14 @@ export type StateProps = {
|
||||
hasActiveCall: boolean;
|
||||
i18n: LocalizerType;
|
||||
isAdmin: boolean;
|
||||
isEditMemberLabelEnabled: boolean;
|
||||
isGroup: boolean;
|
||||
isSignalConversation: boolean;
|
||||
groupsInCommon: ReadonlyArray<ConversationType>;
|
||||
maxGroupSize: number;
|
||||
maxRecommendedGroupSize: number;
|
||||
memberships: ReadonlyArray<GroupV2Membership>;
|
||||
memberColors: Map<string, string>;
|
||||
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
|
||||
pendingAvatarDownload?: boolean;
|
||||
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
||||
@@ -136,21 +140,10 @@ type ActionProps = {
|
||||
setMuteExpiration: (id: string, muteExpiresAt: undefined | number) => unknown;
|
||||
showContactModal: (contactId: string, conversationId?: string) => void;
|
||||
showConversation: ShowConversationType;
|
||||
toggleAboutContactModal: (contactId: string) => void;
|
||||
toggleAboutContactModal: (options: ContactModalStateType) => void;
|
||||
toggleAddUserToAnotherGroupModal: (contactId?: string) => void;
|
||||
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
||||
updateGroupAttributes: (
|
||||
conversationId: string,
|
||||
_: Readonly<{
|
||||
avatar?: undefined | Uint8Array;
|
||||
description?: string;
|
||||
title?: string;
|
||||
}>,
|
||||
opts: {
|
||||
onSuccess?: () => unknown;
|
||||
onFailure?: () => unknown;
|
||||
}
|
||||
) => unknown;
|
||||
updateGroupAttributes: UpdateGroupAttributesType;
|
||||
};
|
||||
|
||||
export type Props = StateProps & ActionProps;
|
||||
@@ -188,10 +181,12 @@ export function ConversationDetails({
|
||||
hasActiveCall,
|
||||
i18n,
|
||||
isAdmin,
|
||||
isEditMemberLabelEnabled,
|
||||
isGroup,
|
||||
isSignalConversation,
|
||||
leaveGroup,
|
||||
memberships,
|
||||
memberColors,
|
||||
maxGroupSize,
|
||||
maxRecommendedGroupSize,
|
||||
onDeleteNicknameAndNote,
|
||||
@@ -732,6 +727,7 @@ export function ConversationDetails({
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
memberships={memberships}
|
||||
memberColors={memberColors}
|
||||
showContactModal={showContactModal}
|
||||
startAddingNewMembers={() => {
|
||||
setModalState(ModalState.AddingGroupMembers);
|
||||
@@ -759,6 +755,22 @@ export function ConversationDetails({
|
||||
right={hasGroupLink ? i18n('icu:on') : i18n('icu:off')}
|
||||
/>
|
||||
) : null}
|
||||
{canEditGroupInfo && isEditMemberLabelEnabled ? (
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('icu:ConversationDetails--member-label')}
|
||||
icon={IconType.tag}
|
||||
/>
|
||||
}
|
||||
label={i18n('icu:ConversationDetails--member-label')}
|
||||
onClick={() =>
|
||||
pushPanelForConversation({
|
||||
type: PanelType.GroupMemberLabelEditor,
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { BadgeType } from '../../../badges/types.std.js';
|
||||
import { UserText } from '../../UserText.dom.js';
|
||||
import { isInSystemContacts } from '../../../util/isInSystemContacts.std.js';
|
||||
import { InContactsIcon } from '../../InContactsIcon.dom.js';
|
||||
import type { ContactModalStateType } from '../../../state/ducks/globalModals.preload.js';
|
||||
|
||||
export type Props = {
|
||||
areWeASubscriber: boolean;
|
||||
@@ -30,7 +31,7 @@ export type Props = {
|
||||
pendingAvatarDownload: boolean;
|
||||
startAvatarDownload: () => void;
|
||||
startEditing: (isGroupTitle: boolean) => void;
|
||||
toggleAboutContactModal: (contactId: string) => void;
|
||||
toggleAboutContactModal: (options: ContactModalStateType) => void;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
||||
@@ -237,7 +238,7 @@ export function ConversationDetailsHeader({
|
||||
onClick={ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleAboutContactModal(conversation.id);
|
||||
toggleAboutContactModal({ contactId: conversation.id });
|
||||
}}
|
||||
className="ConversationDetailsHeader__about-button"
|
||||
>
|
||||
|
||||
@@ -31,6 +31,7 @@ export enum IconType {
|
||||
'reset' = 'reset',
|
||||
'share' = 'share',
|
||||
'spinner' = 'spinner',
|
||||
'tag' = 'tag',
|
||||
'timer' = 'timer',
|
||||
'trash' = 'trash',
|
||||
'verify' = 'verify',
|
||||
|
||||
+41
-27
@@ -12,9 +12,37 @@ import type {
|
||||
GroupV2Membership,
|
||||
} from './ConversationDetailsMembershipList.dom.js';
|
||||
import { ConversationDetailsMembershipList } from './ConversationDetailsMembershipList.dom.js';
|
||||
import { ContactNameColors } from '../../../types/Colors.std.js';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
const createMemberships = (
|
||||
numberOfMemberships = 10
|
||||
): Array<GroupV2Membership> => {
|
||||
return Array.from(new Array(numberOfMemberships)).map(
|
||||
(_, i): GroupV2Membership => ({
|
||||
isAdmin: i % 4 === 0,
|
||||
labelEmoji: i % 6 === 0 ? '🟢' : undefined,
|
||||
labelString: i % 3 === 0 ? `Task Wrangler ${i}` : undefined,
|
||||
member: getDefaultConversation({
|
||||
isMe: i === 2,
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getMemberColors = (
|
||||
memberships: Array<GroupV2Membership>
|
||||
): Map<string, string> =>
|
||||
new Map(
|
||||
memberships.map((membership, i) => [
|
||||
membership.member.id,
|
||||
ContactNameColors[i],
|
||||
])
|
||||
);
|
||||
|
||||
const defaultMemberships = createMemberships();
|
||||
|
||||
export default {
|
||||
title:
|
||||
'Components/Conversation/ConversationDetails/ConversationDetailsMembershipList',
|
||||
@@ -24,49 +52,37 @@ export default {
|
||||
conversationId: '123',
|
||||
getPreferredBadge: () => undefined,
|
||||
i18n,
|
||||
memberships: [],
|
||||
memberships: defaultMemberships,
|
||||
memberColors: getMemberColors(defaultMemberships),
|
||||
showContactModal: action('showContactModal'),
|
||||
startAddingNewMembers: action('startAddingNewMembers'),
|
||||
theme: ThemeType.light,
|
||||
},
|
||||
} satisfies Meta<Props>;
|
||||
|
||||
const createMemberships = (
|
||||
numberOfMemberships = 10
|
||||
): Array<GroupV2Membership> => {
|
||||
return Array.from(new Array(numberOfMemberships)).map(
|
||||
(_, i): GroupV2Membership => ({
|
||||
isAdmin: i % 3 === 0,
|
||||
member: getDefaultConversation({
|
||||
isMe: i === 2,
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export function Few(args: Props): React.JSX.Element {
|
||||
const memberships = createMemberships(3);
|
||||
const memberships = defaultMemberships.slice(3);
|
||||
return (
|
||||
<ConversationDetailsMembershipList {...args} memberships={memberships} />
|
||||
);
|
||||
}
|
||||
|
||||
export function Limit(args: Props): React.JSX.Element {
|
||||
const memberships = createMemberships(5);
|
||||
const memberships = defaultMemberships.slice(5);
|
||||
return (
|
||||
<ConversationDetailsMembershipList {...args} memberships={memberships} />
|
||||
);
|
||||
}
|
||||
|
||||
export function Limit1(args: Props): React.JSX.Element {
|
||||
const memberships = createMemberships(6);
|
||||
const memberships = defaultMemberships.slice(6);
|
||||
return (
|
||||
<ConversationDetailsMembershipList {...args} memberships={memberships} />
|
||||
);
|
||||
}
|
||||
|
||||
export function Limit2(args: Props): React.JSX.Element {
|
||||
const memberships = createMemberships(7);
|
||||
const memberships = defaultMemberships.slice(7);
|
||||
return (
|
||||
<ConversationDetailsMembershipList {...args} memberships={memberships} />
|
||||
);
|
||||
@@ -74,8 +90,13 @@ export function Limit2(args: Props): React.JSX.Element {
|
||||
|
||||
export function Many(args: Props): React.JSX.Element {
|
||||
const memberships = createMemberships(100);
|
||||
const memberColors = getMemberColors(memberships);
|
||||
return (
|
||||
<ConversationDetailsMembershipList {...args} memberships={memberships} />
|
||||
<ConversationDetailsMembershipList
|
||||
{...args}
|
||||
memberships={memberships}
|
||||
memberColors={memberColors}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,12 +105,5 @@ export function None(args: Props): React.JSX.Element {
|
||||
}
|
||||
|
||||
export function CanAddNewMembers(args: Props): React.JSX.Element {
|
||||
const memberships = createMemberships(10);
|
||||
return (
|
||||
<ConversationDetailsMembershipList
|
||||
{...args}
|
||||
memberships={memberships}
|
||||
canAddNewMembers
|
||||
/>
|
||||
);
|
||||
return <ConversationDetailsMembershipList {...args} canAddNewMembers />;
|
||||
}
|
||||
|
||||
+48
-19
@@ -16,10 +16,13 @@ import type { ConversationType } from '../../../state/ducks/conversations.preloa
|
||||
import type { PreferredBadgeSelectorType } from '../../../state/selectors/badges.preload.js';
|
||||
import { PanelRow } from './PanelRow.dom.js';
|
||||
import { PanelSection } from './PanelSection.dom.js';
|
||||
import { GroupMemberLabel } from '../ContactName.dom.js';
|
||||
|
||||
export type GroupV2Membership = {
|
||||
isAdmin: boolean;
|
||||
member: ConversationType;
|
||||
labelEmoji: string | undefined;
|
||||
labelString: string | undefined;
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
@@ -29,6 +32,7 @@ export type Props = {
|
||||
i18n: LocalizerType;
|
||||
maxShownMemberCount?: number;
|
||||
memberships: ReadonlyArray<GroupV2Membership>;
|
||||
memberColors: Map<string, string>;
|
||||
showContactModal: (contactId: string, conversationId?: string) => void;
|
||||
startAddingNewMembers?: () => void;
|
||||
theme: ThemeType;
|
||||
@@ -79,6 +83,7 @@ export function ConversationDetailsMembershipList({
|
||||
getPreferredBadge,
|
||||
i18n,
|
||||
maxShownMemberCount = 5,
|
||||
memberColors,
|
||||
memberships,
|
||||
showContactModal,
|
||||
startAddingNewMembers,
|
||||
@@ -109,26 +114,50 @@ export function ConversationDetailsMembershipList({
|
||||
onClick={() => startAddingNewMembers?.()}
|
||||
/>
|
||||
)}
|
||||
{sortedMemberships.slice(0, membersToShow).map(({ isAdmin, member }) => (
|
||||
<PanelRow
|
||||
key={member.id}
|
||||
onClick={() => showContactModal(member.id, conversationId)}
|
||||
icon={
|
||||
<Avatar
|
||||
conversationType="direct"
|
||||
badge={getPreferredBadge(member.badges)}
|
||||
i18n={i18n}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
theme={theme}
|
||||
{...member}
|
||||
{sortedMemberships
|
||||
.slice(0, membersToShow)
|
||||
.map(({ isAdmin, member, labelEmoji, labelString }) => {
|
||||
const contactNameColor = memberColors.get(member.id);
|
||||
|
||||
return (
|
||||
<PanelRow
|
||||
key={member.id}
|
||||
onClick={() => showContactModal(member.id, conversationId)}
|
||||
icon={
|
||||
<Avatar
|
||||
conversationType="direct"
|
||||
badge={getPreferredBadge(member.badges)}
|
||||
i18n={i18n}
|
||||
size={AvatarSize.THIRTY_SIX}
|
||||
theme={theme}
|
||||
{...member}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<div>
|
||||
<div>
|
||||
<Emojify
|
||||
text={member.isMe ? i18n('icu:you') : member.title}
|
||||
/>
|
||||
</div>
|
||||
{labelString && contactNameColor && (
|
||||
<div>
|
||||
<GroupMemberLabel
|
||||
contactNameColor={contactNameColor}
|
||||
contactLabel={{
|
||||
labelEmoji,
|
||||
labelString,
|
||||
}}
|
||||
context="list"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
right={isAdmin ? i18n('icu:GroupV2--admin') : ''}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Emojify text={member.isMe ? i18n('icu:you') : member.title} />
|
||||
}
|
||||
right={isAdmin ? i18n('icu:GroupV2--admin') : ''}
|
||||
/>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
{showAllMembers === false && shouldHideRestMembers && (
|
||||
<PanelRow
|
||||
className="ConversationDetails-membership-list--show-all"
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './GroupMemberLabelEditor.dom.js';
|
||||
import { GroupMemberLabelEditor } from './GroupMemberLabelEditor.dom.js';
|
||||
import type { ConversationType } from '../../../state/ducks/conversations.preload.js';
|
||||
import { getDefaultConversation } from '../../../test-helpers/getDefaultConversation.std.js';
|
||||
import { ThemeType } from '../../../types/Util.std.js';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/ConversationDetails/GroupMemberLabelEditor',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
const createProps = (conversation?: ConversationType): PropsType => ({
|
||||
conversation: conversation || getDefaultConversation({ type: 'group' }),
|
||||
existingLabelEmoji: '🐘',
|
||||
existingLabelString: 'Good Memory',
|
||||
i18n,
|
||||
popPanelForConversation: action('popPanelForConversation'),
|
||||
theme: ThemeType.light,
|
||||
updateGroupMemberLabel: action('changeHasGroupLink'),
|
||||
});
|
||||
|
||||
export function NoExistingLabel(): React.JSX.Element {
|
||||
const props = {
|
||||
...createProps(),
|
||||
existingLabelEmoji: undefined,
|
||||
existingLabelString: undefined,
|
||||
};
|
||||
|
||||
return <GroupMemberLabelEditor {...props} />;
|
||||
}
|
||||
|
||||
export function ExistingLabel(): React.JSX.Element {
|
||||
const props = createProps();
|
||||
|
||||
return <GroupMemberLabelEditor {...props} />;
|
||||
}
|
||||
|
||||
export function StringButNoEmoji(): React.JSX.Element {
|
||||
const props = {
|
||||
...createProps(),
|
||||
existingLabelEmoji: undefined,
|
||||
};
|
||||
|
||||
return <GroupMemberLabelEditor {...props} />;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Input } from '../../Input.dom.js';
|
||||
import { FunEmojiPicker } from '../../fun/FunEmojiPicker.dom.js';
|
||||
import {
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from '../../fun/data/emojis.std.js';
|
||||
import { FunEmojiPickerButton } from '../../fun/FunButton.dom.js';
|
||||
|
||||
import type { EmojiVariantKey } from '../../fun/data/emojis.std.js';
|
||||
import type {
|
||||
ConversationType,
|
||||
UpdateGroupMemberLabelType,
|
||||
} from '../../../state/ducks/conversations.preload.js';
|
||||
import type { LocalizerType, ThemeType } from '../../../types/Util.std.js';
|
||||
import { tw } from '../../../axo/tw.dom.js';
|
||||
import { AxoButton } from '../../../axo/AxoButton.dom.js';
|
||||
|
||||
export type PropsDataType = {
|
||||
conversation: ConversationType;
|
||||
existingLabelEmoji: string | undefined;
|
||||
existingLabelString: string | undefined;
|
||||
i18n: LocalizerType;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
||||
export type PropsType = PropsDataType & {
|
||||
popPanelForConversation: () => void;
|
||||
updateGroupMemberLabel: UpdateGroupMemberLabelType;
|
||||
};
|
||||
|
||||
function getEmojiVariantKey(value: string): EmojiVariantKey | undefined {
|
||||
if (isEmojiVariantValue(value)) {
|
||||
return getEmojiVariantKeyByValue(value);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function GroupMemberLabelEditor({
|
||||
conversation,
|
||||
existingLabelEmoji,
|
||||
existingLabelString,
|
||||
i18n,
|
||||
popPanelForConversation,
|
||||
theme,
|
||||
updateGroupMemberLabel,
|
||||
}: PropsType): React.JSX.Element {
|
||||
const [labelEmoji, setLabelEmoji] = useState(existingLabelEmoji);
|
||||
const [labelString, setLabelString] = useState(existingLabelString);
|
||||
|
||||
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
|
||||
|
||||
const emojiKey = labelEmoji ? getEmojiVariantKey(labelEmoji) : null;
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const isDirty =
|
||||
labelEmoji !== existingLabelEmoji || labelString !== existingLabelString;
|
||||
const spinner = isSaving
|
||||
? {
|
||||
'aria-label': i18n('icu:ConversationDetails--member-label--saving'),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className={tw('mx-auto flex h-full max-w-[640px] flex-col')}>
|
||||
<div>
|
||||
<Input
|
||||
autoFocus
|
||||
hasClearButton
|
||||
i18n={i18n}
|
||||
icon={
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={(open: boolean) => setEmojiPickerOpen(open)}
|
||||
placement="bottom"
|
||||
onSelectEmoji={data => {
|
||||
const newEmoji = getEmojiVariantByKey(data.variantKey)?.value;
|
||||
|
||||
setLabelEmoji(newEmoji);
|
||||
}}
|
||||
closeOnSelect
|
||||
theme={theme}
|
||||
>
|
||||
<FunEmojiPickerButton i18n={i18n} selectedEmoji={emojiKey} />
|
||||
</FunEmojiPicker>
|
||||
}
|
||||
maxLengthCount={24}
|
||||
maxByteCount={96}
|
||||
moduleClassName="GroupMemberLabelEditor"
|
||||
onChange={value => {
|
||||
if (!value) {
|
||||
setLabelEmoji(undefined);
|
||||
}
|
||||
setLabelString(value);
|
||||
}}
|
||||
ref={undefined}
|
||||
placeholder={i18n(
|
||||
'icu:ConversationDetails--member-label--placeholder'
|
||||
)}
|
||||
value={labelString}
|
||||
whenToShowRemainingCount={20}
|
||||
/>
|
||||
</div>
|
||||
<div className={tw('text-label-secondary')}>
|
||||
{i18n('icu:ConversationDetails--member-label--description')}
|
||||
</div>
|
||||
|
||||
<div className={tw('flex-grow')} />
|
||||
<div className={tw('mb-3 flex w-full justify-end gap-2')}>
|
||||
<AxoButton.Root
|
||||
variant="secondary"
|
||||
size="md"
|
||||
onClick={() => {
|
||||
popPanelForConversation();
|
||||
}}
|
||||
>
|
||||
{i18n('icu:cancel')}
|
||||
</AxoButton.Root>
|
||||
|
||||
<AxoButton.Root
|
||||
variant="primary"
|
||||
size="md"
|
||||
experimentalSpinner={spinner}
|
||||
disabled={!isDirty || isSaving}
|
||||
onClick={() => {
|
||||
setIsSaving(true);
|
||||
updateGroupMemberLabel(
|
||||
{
|
||||
conversationId: conversation.id,
|
||||
labelEmoji,
|
||||
labelString,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
setIsSaving(false);
|
||||
popPanelForConversation();
|
||||
},
|
||||
onFailure() {
|
||||
// TODO: DESKTOP-9698
|
||||
},
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:save')}
|
||||
</AxoButton.Root>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -39,6 +39,7 @@ function getEmojiJumboBackground(
|
||||
|
||||
export type FunStaticEmojiSize =
|
||||
| 12
|
||||
| 14
|
||||
| 16
|
||||
| 18
|
||||
| 20
|
||||
@@ -62,6 +63,7 @@ export enum FunJumboEmojiSize {
|
||||
|
||||
const funStaticEmojiSizeClasses = {
|
||||
12: 'FunStaticEmoji--Size12',
|
||||
14: 'FunStaticEmoji--Size14',
|
||||
16: 'FunStaticEmoji--Size16',
|
||||
18: 'FunStaticEmoji--Size18',
|
||||
20: 'FunStaticEmoji--Size20',
|
||||
|
||||
+229
-4
@@ -211,7 +211,7 @@ const GROUP_DESC_MAX_ENCRYPTED_BYTES = 8192;
|
||||
const TEMPORAL_AUTH_REJECTED_CODE = 401;
|
||||
const GROUP_ACCESS_DENIED_CODE = 403;
|
||||
const GROUP_NONEXISTENT_CODE = 404;
|
||||
const SUPPORTED_CHANGE_EPOCH = 5;
|
||||
const SUPPORTED_CHANGE_EPOCH = 6; // support for ModifyMemberLabelAction
|
||||
export const LINK_VERSION_ERROR = 'LINK_VERSION_ERROR';
|
||||
const GROUP_INVITE_LINK_PASSWORD_LENGTH = 16;
|
||||
|
||||
@@ -849,16 +849,59 @@ export function buildAnnouncementsOnlyChange(
|
||||
|
||||
export function buildAccessControlAttributesChange(
|
||||
group: ConversationAttributesType,
|
||||
value: AccessRequiredEnum
|
||||
newValue: AccessRequiredEnum
|
||||
): Proto.GroupChange.Actions {
|
||||
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
|
||||
const ROLE_ENUM = Proto.Member.Role;
|
||||
|
||||
const accessControlAction =
|
||||
new Proto.GroupChange.Actions.ModifyAttributesAccessControlAction();
|
||||
accessControlAction.attributesAccess = value;
|
||||
accessControlAction.attributesAccess = newValue;
|
||||
|
||||
if (!group.secretParams) {
|
||||
throw new Error(
|
||||
'buildAccessControlAttributesChange: group was missing secretParams!'
|
||||
);
|
||||
}
|
||||
|
||||
const actions = new Proto.GroupChange.Actions();
|
||||
actions.version = (group.revision || 0) + 1;
|
||||
actions.modifyAttributesAccess = accessControlAction;
|
||||
|
||||
// Clear out all non-admin labels
|
||||
const previousValue = group.accessControl?.attributes;
|
||||
if (
|
||||
previousValue !== ACCESS_ENUM.ADMINISTRATOR &&
|
||||
newValue === ACCESS_ENUM.ADMINISTRATOR
|
||||
) {
|
||||
const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams);
|
||||
|
||||
const modifyLabelActions = (group.membersV2 || [])
|
||||
.map(member => {
|
||||
if (member.role === ROLE_ENUM.ADMINISTRATOR) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!member.labelString && !member.labelEmoji) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const modifyLabel =
|
||||
new Proto.GroupChange.Actions.ModifyMemberLabelAction();
|
||||
modifyLabel.userId = encryptServiceId(clientZkGroupCipher, member.aci);
|
||||
|
||||
return modifyLabel;
|
||||
})
|
||||
.filter(isNotNil);
|
||||
|
||||
if (modifyLabelActions.length) {
|
||||
log.info(
|
||||
`buildAccessControlAttributesChange: Found ${modifyLabelActions.length} non-admins with labels. Clearing.`
|
||||
);
|
||||
actions.modifyMemberLabels = modifyLabelActions;
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
@@ -1207,7 +1250,9 @@ export function buildModifyMemberRoleChange({
|
||||
const actions = new Proto.GroupChange.Actions();
|
||||
|
||||
if (!group.secretParams) {
|
||||
throw new Error('buildMakeAdminChange: group was missing secretParams!');
|
||||
throw new Error(
|
||||
'buildModifyMemberRoleChange: group was missing secretParams!'
|
||||
);
|
||||
}
|
||||
|
||||
const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams);
|
||||
@@ -1223,6 +1268,49 @@ export function buildModifyMemberRoleChange({
|
||||
return actions;
|
||||
}
|
||||
|
||||
export function buildModifyMemberLabelChange({
|
||||
serviceId,
|
||||
group,
|
||||
labelEmoji,
|
||||
labelString,
|
||||
}: {
|
||||
serviceId: ServiceIdString;
|
||||
group: ConversationAttributesType;
|
||||
labelEmoji: string | undefined;
|
||||
labelString: string | undefined;
|
||||
}): Proto.GroupChange.Actions {
|
||||
const actions = new Proto.GroupChange.Actions();
|
||||
|
||||
if (!group.secretParams) {
|
||||
throw new Error(
|
||||
'buildModifyMemberLabelChange: group was missing secretParams!'
|
||||
);
|
||||
}
|
||||
|
||||
const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams);
|
||||
const userIdCipherText = encryptServiceId(clientZkGroupCipher, serviceId);
|
||||
|
||||
const modifyLabel = new Proto.GroupChange.Actions.ModifyMemberLabelAction();
|
||||
modifyLabel.userId = userIdCipherText;
|
||||
if (labelEmoji) {
|
||||
modifyLabel.labelEmoji = encryptGroupBlob(
|
||||
clientZkGroupCipher,
|
||||
Bytes.fromString(labelEmoji)
|
||||
);
|
||||
}
|
||||
if (labelString) {
|
||||
modifyLabel.labelString = encryptGroupBlob(
|
||||
clientZkGroupCipher,
|
||||
Bytes.fromString(labelString)
|
||||
);
|
||||
}
|
||||
|
||||
actions.version = (group.revision || 0) + 1;
|
||||
actions.modifyMemberLabels = [modifyLabel];
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
export function buildPromotePendingAdminApprovalMemberChange({
|
||||
group,
|
||||
aci,
|
||||
@@ -5128,6 +5216,28 @@ async function applyGroupChange({
|
||||
}
|
||||
});
|
||||
|
||||
// modifyMemberLabels?: Array<GroupChange.Actions.ModifyMemberLabelAction>;
|
||||
(actions.modifyMemberLabels || []).forEach(modifyMemberLabel => {
|
||||
const { userId, labelEmoji, labelString } = modifyMemberLabel;
|
||||
if (!userId) {
|
||||
throw new Error(
|
||||
'applyGroupChange: modifyMemberLabel had a missing userId'
|
||||
);
|
||||
}
|
||||
|
||||
if (members[userId]) {
|
||||
members[userId] = {
|
||||
...members[userId],
|
||||
labelEmoji,
|
||||
labelString,
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
'applyGroupChange: modifyMemberLabel tried to modify nonexistent member'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// modifyMemberProfileKeys?:
|
||||
// Array<GroupChange.Actions.ModifyMemberProfileKeyAction>;
|
||||
(actions.modifyMemberProfileKeys || []).forEach(modifyMemberProfileKey => {
|
||||
@@ -5848,6 +5958,8 @@ async function applyGroupState({
|
||||
role: member.role || MEMBER_ROLE_ENUM.DEFAULT,
|
||||
joinedAtVersion: member.joinedAtVersion,
|
||||
aci: member.userId,
|
||||
labelEmoji: member.labelEmoji,
|
||||
labelString: member.labelString,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -6051,6 +6163,12 @@ function normalizeTimestamp(timestamp: Long | null | undefined): number {
|
||||
return asNumber;
|
||||
}
|
||||
|
||||
type DecryptedModifyMemberLabelAction = {
|
||||
userId: AciString;
|
||||
labelEmoji?: string;
|
||||
labelString?: string;
|
||||
};
|
||||
|
||||
type DecryptedGroupChangeActions = {
|
||||
version?: number;
|
||||
sourceServiceId?: ServiceIdString;
|
||||
@@ -6065,6 +6183,7 @@ type DecryptedGroupChangeActions = {
|
||||
userId: AciString;
|
||||
role: Proto.Member.Role;
|
||||
}>;
|
||||
modifyMemberLabels?: ReadonlyArray<DecryptedModifyMemberLabelAction>;
|
||||
modifyMemberProfileKeys?: ReadonlyArray<{
|
||||
profileKey: Uint8Array;
|
||||
aci: AciString;
|
||||
@@ -6236,6 +6355,17 @@ function decryptGroupChange(
|
||||
})
|
||||
);
|
||||
|
||||
// modifyMemberLabels?: Array<GroupChange.Actions.ModifyMemberLabelAction>
|
||||
result.modifyMemberLabels = compact(
|
||||
(actions.modifyMemberLabels || []).map(modifyMemberLabel =>
|
||||
decryptModifyMemberLabelAction(
|
||||
clientZkGroupCipher,
|
||||
modifyMemberLabel,
|
||||
logId
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// modifyMemberProfileKeys?: Array<
|
||||
// GroupChange.Actions.ModifyMemberProfileKeyAction
|
||||
// >;
|
||||
@@ -6785,6 +6915,67 @@ export function decryptGroupDescription(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function decryptModifyMemberLabelAction(
|
||||
clientZkGroupCipher: ClientZkGroupCipher,
|
||||
modifyMember: Readonly<Proto.GroupChange.Actions.IModifyMemberLabelAction>,
|
||||
logId: string
|
||||
): DecryptedModifyMemberLabelAction | undefined {
|
||||
const { userId, labelEmoji, labelString } = modifyMember;
|
||||
|
||||
// userId
|
||||
strictAssert(
|
||||
Bytes.isNotEmpty(userId),
|
||||
'decryptModifyMemberLabelAction: Missing userId'
|
||||
);
|
||||
|
||||
let decryptedUserId: AciString;
|
||||
try {
|
||||
decryptedUserId = decryptAci(clientZkGroupCipher, userId);
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
`decryptModifyMemberLabelAction/${logId}: Unable to decrypt pending member userId. Dropping member.`,
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// labelEmoji
|
||||
let decryptedLabelEmoji: string | undefined;
|
||||
if (Bytes.isNotEmpty(labelEmoji)) {
|
||||
try {
|
||||
decryptedLabelEmoji = Bytes.toString(
|
||||
decryptGroupBlob(clientZkGroupCipher, labelEmoji)
|
||||
);
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
`decryptMemberPendingAdminApproval/${logId}: Unable to decrypt labelEmoji. Dropping it.`,
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// labelString
|
||||
let decryptedLabelString: string | undefined;
|
||||
if (Bytes.isNotEmpty(labelString)) {
|
||||
try {
|
||||
decryptedLabelString = Bytes.toString(
|
||||
decryptGroupBlob(clientZkGroupCipher, labelString)
|
||||
);
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
`decryptMemberPendingAdminApproval/${logId}: Unable to decrypt labelString. Dropping it.`,
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
userId: decryptedUserId,
|
||||
labelEmoji: decryptedLabelEmoji,
|
||||
labelString: decryptedLabelString,
|
||||
};
|
||||
}
|
||||
|
||||
type DecryptedGroupState = {
|
||||
title?: Proto.GroupAttributeBlob;
|
||||
disappearingMessagesTimer?: Proto.GroupAttributeBlob;
|
||||
@@ -6973,6 +7164,8 @@ type DecryptedMember = Readonly<{
|
||||
profileKey: Uint8Array;
|
||||
role: Proto.Member.Role;
|
||||
joinedAtVersion: number;
|
||||
labelEmoji?: string;
|
||||
labelString?: string;
|
||||
}>;
|
||||
|
||||
function decryptMember(
|
||||
@@ -7019,11 +7212,43 @@ function decryptMember(
|
||||
throw new Error(`decryptMember: Member had invalid role ${member.role}`);
|
||||
}
|
||||
|
||||
// labelEmoji
|
||||
let decryptedLabelEmoji: string | undefined;
|
||||
if (Bytes.isNotEmpty(member.labelEmoji)) {
|
||||
try {
|
||||
decryptedLabelEmoji = Bytes.toString(
|
||||
decryptGroupBlob(clientZkGroupCipher, member.labelEmoji)
|
||||
);
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
`decryptMemberPendingAdminApproval/${logId}: Unable to decrypt labelEmoji. Dropping it.`,
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// labelString
|
||||
let decryptedLabelString: string | undefined;
|
||||
if (Bytes.isNotEmpty(member.labelString)) {
|
||||
try {
|
||||
decryptedLabelString = Bytes.toString(
|
||||
decryptGroupBlob(clientZkGroupCipher, member.labelString)
|
||||
);
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
`decryptMemberPendingAdminApproval/${logId}: Unable to decrypt labelString. Dropping it.`,
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
userId,
|
||||
profileKey,
|
||||
role,
|
||||
joinedAtVersion: dropNull(member.joinedAtVersion) ?? 0,
|
||||
labelEmoji: decryptedLabelEmoji,
|
||||
labelString: decryptedLabelString,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Vendored
+2
@@ -557,6 +557,8 @@ export type GroupV2MemberType = {
|
||||
aci: AciString;
|
||||
role: MemberRoleEnum;
|
||||
joinedAtVersion: number;
|
||||
labelString?: string;
|
||||
labelEmoji?: string;
|
||||
|
||||
// Note that these are temporary flags, generated by applyGroupChange, but eliminated
|
||||
// by applyGroupState. They are used to make our diff-generation more intelligent but
|
||||
|
||||
@@ -246,6 +246,7 @@ import {
|
||||
buildDisappearingMessagesTimerChange,
|
||||
buildGroupLink,
|
||||
buildInviteLinkPasswordChange,
|
||||
buildModifyMemberLabelChange,
|
||||
buildModifyMemberRoleChange,
|
||||
buildNewGroupLinkChange,
|
||||
buildPromoteMemberChange,
|
||||
@@ -4579,6 +4580,34 @@ export class ConversationModel {
|
||||
}
|
||||
}
|
||||
|
||||
async updateGroupMemberLabel({
|
||||
labelEmoji,
|
||||
labelString,
|
||||
}: {
|
||||
labelEmoji: string | undefined;
|
||||
labelString: string | undefined;
|
||||
}): Promise<void> {
|
||||
if (!isGroupV2(this.attributes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('updateGroupMemberLabel for conversation', this.idForLogging());
|
||||
|
||||
const ourServiceId = itemStorage.user.getCheckedAci();
|
||||
|
||||
await this.modifyGroupV2({
|
||||
name: 'updateGroupMemberLabel',
|
||||
usingCredentialsFrom: [],
|
||||
createGroupChange: async () =>
|
||||
buildModifyMemberLabelChange({
|
||||
serviceId: ourServiceId,
|
||||
group: this.attributes,
|
||||
labelEmoji,
|
||||
labelString,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async refreshGroupLink(): Promise<void> {
|
||||
if (!isGroupV2(this.attributes)) {
|
||||
return;
|
||||
|
||||
@@ -396,6 +396,8 @@ export type ConversationType = ReadonlyDeep<
|
||||
memberships?: ReadonlyArray<{
|
||||
aci: AciString;
|
||||
isAdmin: boolean;
|
||||
labelEmoji: string | undefined;
|
||||
labelString: string | undefined;
|
||||
}>;
|
||||
pendingMemberships?: ReadonlyArray<{
|
||||
serviceId: ServiceIdString;
|
||||
@@ -1341,6 +1343,7 @@ export const actions = {
|
||||
toggleSelectMessage,
|
||||
toggleSelectMode,
|
||||
updateGroupAttributes,
|
||||
updateGroupMemberLabel,
|
||||
updateLastMessage,
|
||||
updateNicknameAndNote,
|
||||
verifyConversationsStoppingSend,
|
||||
@@ -4636,6 +4639,14 @@ function addMembersToGroup(
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type ActionCreator<T extends (...params: Array<any>) => any> = ReadonlyDeep<
|
||||
(...params: Parameters<T>) => void
|
||||
>;
|
||||
|
||||
export type UpdateGroupAttributesType = ReadonlyDeep<
|
||||
ActionCreator<typeof updateGroupAttributes>
|
||||
>;
|
||||
function updateGroupAttributes(
|
||||
conversationId: string,
|
||||
attributes: Readonly<{
|
||||
@@ -4678,6 +4689,47 @@ function updateGroupAttributes(
|
||||
};
|
||||
}
|
||||
|
||||
export type UpdateGroupMemberLabelType = ReadonlyDeep<
|
||||
ActionCreator<typeof updateGroupMemberLabel>
|
||||
>;
|
||||
function updateGroupMemberLabel(
|
||||
{
|
||||
conversationId,
|
||||
labelEmoji,
|
||||
labelString,
|
||||
}: {
|
||||
conversationId: string;
|
||||
labelEmoji: string | undefined;
|
||||
labelString: string | undefined;
|
||||
},
|
||||
{
|
||||
onSuccess,
|
||||
onFailure,
|
||||
}: {
|
||||
onSuccess?: () => unknown;
|
||||
onFailure?: () => unknown;
|
||||
} = {}
|
||||
): ThunkAction<void, RootStateType, unknown, never> {
|
||||
return async () => {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
throw new Error('updateGroupMemberLabel: No conversation found');
|
||||
}
|
||||
|
||||
try {
|
||||
await longRunningTaskWrapper({
|
||||
name: 'updateGroupMemberLabel',
|
||||
idForLogging: conversation.idForLogging(),
|
||||
task: async () =>
|
||||
conversation.updateGroupMemberLabel({ labelEmoji, labelString }),
|
||||
});
|
||||
onSuccess?.();
|
||||
} catch {
|
||||
onFailure?.();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function leaveGroup(
|
||||
conversationId: string
|
||||
): ThunkAction<void, RootStateType, unknown, never> {
|
||||
|
||||
@@ -116,7 +116,7 @@ export type CallQualitySurveyPropsType = ReadonlyDeep<{
|
||||
|
||||
export type GlobalModalsStateType = ReadonlyDeep<{
|
||||
addUserToAnotherGroupModalContactId?: string;
|
||||
aboutContactModalContactId?: string;
|
||||
aboutContactModalState?: ContactModalStateType;
|
||||
backfillFailureModalProps: BackfillFailureModalPropsType | undefined;
|
||||
callLinkAddNameModalRoomId: string | null;
|
||||
callLinkEditModalRoomId: string | null;
|
||||
@@ -395,7 +395,7 @@ export type HideCallQualitySurveyActionType = ReadonlyDeep<{
|
||||
|
||||
type ToggleAboutContactModalActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_ABOUT_MODAL;
|
||||
payload: string | undefined;
|
||||
payload: ContactModalStateType | undefined;
|
||||
}>;
|
||||
|
||||
type ToggleSignalConnectionsModalActionType = ReadonlyDeep<{
|
||||
@@ -1105,11 +1105,11 @@ function toggleCallLinkPendingParticipantModal(
|
||||
}
|
||||
|
||||
function toggleAboutContactModal(
|
||||
contactId?: string
|
||||
payload?: ContactModalStateType
|
||||
): ToggleAboutContactModalActionType {
|
||||
return {
|
||||
type: TOGGLE_ABOUT_MODAL,
|
||||
payload: contactId,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1505,7 +1505,7 @@ export function reducer(
|
||||
if (action.type === TOGGLE_ABOUT_MODAL) {
|
||||
return {
|
||||
...state,
|
||||
aboutContactModalContactId: action.payload,
|
||||
aboutContactModalState: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1639,7 +1639,7 @@ export function reducer(
|
||||
if (action.payload.contactId === ourId) {
|
||||
return {
|
||||
...state,
|
||||
aboutContactModalContactId: ourId,
|
||||
aboutContactModalState: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1361,10 +1361,15 @@ export function isMissingRequiredProfileSharing(
|
||||
);
|
||||
}
|
||||
|
||||
export type AdminMembershipType = {
|
||||
member: ConversationType;
|
||||
labelEmoji: string | undefined;
|
||||
labelString: string | undefined;
|
||||
};
|
||||
export const getGroupAdminsSelector = createSelector(
|
||||
getConversationSelector,
|
||||
(conversationSelector: GetConversationByIdType) => {
|
||||
return (conversationId: string): Array<ConversationType> => {
|
||||
return (conversationId: string): Array<AdminMembershipType> => {
|
||||
const {
|
||||
groupId,
|
||||
groupVersion,
|
||||
@@ -1380,11 +1385,15 @@ export const getGroupAdminsSelector = createSelector(
|
||||
return [];
|
||||
}
|
||||
|
||||
const admins: Array<ConversationType> = [];
|
||||
const admins: Array<AdminMembershipType> = [];
|
||||
memberships.forEach(membership => {
|
||||
if (membership.isAdmin) {
|
||||
const admin = conversationSelector(membership.aci);
|
||||
admins.push(admin);
|
||||
admins.push({
|
||||
member: admin,
|
||||
labelEmoji: membership.labelEmoji,
|
||||
labelString: membership.labelString,
|
||||
});
|
||||
}
|
||||
});
|
||||
return admins;
|
||||
|
||||
@@ -50,6 +50,11 @@ export const getConfirmLeaveCallModalState = createSelector(
|
||||
({ confirmLeaveCallModalState }) => confirmLeaveCallModalState
|
||||
);
|
||||
|
||||
export const getAboutContactModalState = createSelector(
|
||||
getGlobalModalsState,
|
||||
({ aboutContactModalState }) => aboutContactModalState
|
||||
);
|
||||
|
||||
export const getContactModalState = createSelector(
|
||||
getGlobalModalsState,
|
||||
({ contactModalState }) => contactModalState
|
||||
|
||||
@@ -905,6 +905,17 @@ export const getPropsForMessage = (
|
||||
ourAci,
|
||||
});
|
||||
const contactNameColor = getContactNameColor(contactNameColors, authorId);
|
||||
const sourceServiceId = getSourceServiceId(message, ourAci);
|
||||
// TODO: DESKTOP-9698
|
||||
const sourceMember = conversation.memberships?.find(
|
||||
membership => membership.aci === sourceServiceId
|
||||
);
|
||||
const contactLabel = sourceMember?.labelString
|
||||
? {
|
||||
labelString: sourceMember.labelString,
|
||||
labelEmoji: sourceMember.labelEmoji,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const { conversationColor, customColor } = getConversationColorAttributes(
|
||||
conversation,
|
||||
@@ -940,6 +951,7 @@ export const getPropsForMessage = (
|
||||
canRetry: hasErrors(message),
|
||||
canRetryDeleteForEveryone: canRetryDeleteForEveryone(message),
|
||||
contact: getPropsForEmbeddedContact(message, regionCode, accountSelector),
|
||||
contactLabel,
|
||||
contactNameColor,
|
||||
conversationColor,
|
||||
conversationId,
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { AboutContactModal } from '../../components/conversation/AboutContactModal.dom.js';
|
||||
import { isSignalConnection } from '../../util/getSignalConnections.preload.js';
|
||||
import { getIntl } from '../selectors/user.std.js';
|
||||
import { getGlobalModalsState } from '../selectors/globalModals.std.js';
|
||||
import { getIntl, getVersion } from '../selectors/user.std.js';
|
||||
import { getAboutContactModalState } from '../selectors/globalModals.std.js';
|
||||
import {
|
||||
getCachedConversationMemberColorsSelector,
|
||||
getConversationSelector,
|
||||
getPendingAvatarDownloadSelector,
|
||||
} from '../selectors/conversations.dom.js';
|
||||
@@ -16,6 +18,9 @@ import { useConversationsActions } from '../ducks/conversations.preload.js';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals.preload.js';
|
||||
import { strictAssert } from '../../util/assert.std.js';
|
||||
import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation.preload.js';
|
||||
import { SignalService as Proto } from '../../protobuf/index.std.js';
|
||||
import { getItems } from '../selectors/items.dom.js';
|
||||
import { isFeaturedEnabledSelector } from '../../util/isFeatureEnabled.dom.js';
|
||||
|
||||
function isFromOrAddedByTrustedContact(
|
||||
conversation: ConversationType
|
||||
@@ -36,16 +41,43 @@ function isFromOrAddedByTrustedContact(
|
||||
|
||||
export const SmartAboutContactModal = memo(function SmartAboutContactModal() {
|
||||
const i18n = useSelector(getIntl);
|
||||
const globalModals = useSelector(getGlobalModalsState);
|
||||
const { aboutContactModalContactId: contactId } = globalModals;
|
||||
const version = useSelector(getVersion);
|
||||
const items = useSelector(getItems);
|
||||
const { conversationId, contactId } =
|
||||
useSelector(getAboutContactModalState) ?? {};
|
||||
const getConversation = useSelector(getConversationSelector);
|
||||
const isPendingAvatarDownload = useSelector(getPendingAvatarDownloadSelector);
|
||||
|
||||
const isEditMemberLabelEnabled = isFeaturedEnabledSelector({
|
||||
betaKey: 'desktop.groupMemberLabels.edit.beta',
|
||||
currentVersion: version,
|
||||
remoteConfig: items.remoteConfig,
|
||||
prodKey: 'desktop.groupMemberLabels.edit.prod',
|
||||
});
|
||||
|
||||
const sharedGroupNames = useSharedGroupNamesOnMount(contactId ?? '');
|
||||
|
||||
const { startAvatarDownload } = useConversationsActions();
|
||||
|
||||
const conversation = getConversation(contactId);
|
||||
const { id: conversationId } = conversation ?? {};
|
||||
const contact = getConversation(contactId);
|
||||
const conversation = getConversation(conversationId);
|
||||
|
||||
const getMemberColors = useSelector(
|
||||
getCachedConversationMemberColorsSelector
|
||||
);
|
||||
const memberColors = getMemberColors(conversationId);
|
||||
const contactNameColor = memberColors?.get(contact.id);
|
||||
// TODO: DESKTOP-9698
|
||||
const contactMembership = conversation.memberships?.find(
|
||||
membership => contact.serviceId && membership.aci === contact.serviceId
|
||||
);
|
||||
const { labelEmoji: contactLabelEmoji, labelString: contactLabelString } =
|
||||
contactMembership || {};
|
||||
const canAddLabel =
|
||||
conversation.type === 'group' &&
|
||||
(contactMembership?.isAdmin ||
|
||||
conversation.accessControlAttributes ===
|
||||
Proto.AccessControl.AccessRequired.MEMBER);
|
||||
|
||||
const {
|
||||
toggleAboutContactModal,
|
||||
@@ -56,32 +88,37 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() {
|
||||
} = useGlobalModalActions();
|
||||
|
||||
const handleOpenNotePreviewModal = useCallback(() => {
|
||||
strictAssert(conversationId != null, 'conversationId is required');
|
||||
toggleNotePreviewModal({ conversationId });
|
||||
}, [toggleNotePreviewModal, conversationId]);
|
||||
strictAssert(contactId != null, 'contactId is required');
|
||||
toggleNotePreviewModal({ conversationId: contactId });
|
||||
}, [toggleNotePreviewModal, contactId]);
|
||||
|
||||
if (conversation == null) {
|
||||
if (contact == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AboutContactModal
|
||||
i18n={i18n}
|
||||
conversation={conversation}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||
toggleSafetyNumberModal={toggleSafetyNumberModal}
|
||||
isSignalConnection={isSignalConnection(conversation)}
|
||||
fromOrAddedByTrustedContact={isFromOrAddedByTrustedContact(conversation)}
|
||||
canAddLabel={canAddLabel}
|
||||
contact={contact}
|
||||
contactLabelEmoji={contactLabelEmoji}
|
||||
contactLabelString={contactLabelString}
|
||||
contactNameColor={contactNameColor}
|
||||
fromOrAddedByTrustedContact={isFromOrAddedByTrustedContact(contact)}
|
||||
isEditMemberLabelEnabled={isEditMemberLabelEnabled}
|
||||
isSignalConnection={isSignalConnection(contact)}
|
||||
onClose={toggleAboutContactModal}
|
||||
onOpenNotePreviewModal={handleOpenNotePreviewModal}
|
||||
toggleProfileNameWarningModal={toggleProfileNameWarningModal}
|
||||
pendingAvatarDownload={
|
||||
conversationId ? isPendingAvatarDownload(conversationId) : false
|
||||
}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
startAvatarDownload={
|
||||
conversationId ? () => startAvatarDownload(conversationId) : undefined
|
||||
}
|
||||
toggleProfileNameWarningModal={toggleProfileNameWarningModal}
|
||||
toggleSafetyNumberModal={toggleSafetyNumberModal}
|
||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges.preload.js';
|
||||
import { getComposerStateForConversationIdSelector } from '../selectors/composer.preload.js';
|
||||
import {
|
||||
getCachedConversationMemberColorsSelector,
|
||||
getConversationSelector,
|
||||
getGroupAdminsSelector,
|
||||
getHasPanelOpen,
|
||||
@@ -131,6 +132,10 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
|
||||
const groupAdmins = useMemo(() => {
|
||||
return getGroupAdmins(id);
|
||||
}, [getGroupAdmins, id]);
|
||||
const getMemberColors = useSelector(
|
||||
getCachedConversationMemberColorsSelector
|
||||
);
|
||||
const memberColors = getMemberColors(id);
|
||||
|
||||
const addedBy = useMemo(() => {
|
||||
if (conversation.type === 'group') {
|
||||
@@ -229,7 +234,9 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
|
||||
<CompositionArea
|
||||
// Base
|
||||
conversationId={id}
|
||||
draftBodyRanges={hydratedDraftBodyRanges ?? null}
|
||||
draftEditMessage={draftEditMessage ?? null}
|
||||
draftText={conversation.draftText ?? null}
|
||||
focusCounter={focusCounter}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
@@ -322,8 +329,7 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
|
||||
areWePending={conversation.areWePending ?? null}
|
||||
areWePendingApproval={conversation.areWePendingApproval ?? null}
|
||||
groupAdmins={groupAdmins}
|
||||
draftText={conversation.draftText ?? null}
|
||||
draftBodyRanges={hydratedDraftBodyRanges ?? null}
|
||||
memberColors={memberColors}
|
||||
renderSmartCompositionRecording={renderSmartCompositionRecording}
|
||||
renderSmartCompositionRecordingDraft={
|
||||
renderSmartCompositionRecordingDraft
|
||||
|
||||
@@ -7,7 +7,10 @@ import { ContactModal } from '../../components/conversation/ContactModal.dom.js'
|
||||
import { getAreWeASubscriber } from '../selectors/items.dom.js';
|
||||
import { getIntl, getTheme } from '../selectors/user.std.js';
|
||||
import { getBadgesSelector } from '../selectors/badges.preload.js';
|
||||
import { getConversationSelector } from '../selectors/conversations.dom.js';
|
||||
import {
|
||||
getCachedConversationMemberColorsSelector,
|
||||
getConversationSelector,
|
||||
} from '../selectors/conversations.dom.js';
|
||||
import { getHasStoriesSelector } from '../selectors/stories2.dom.js';
|
||||
import {
|
||||
getActiveCallState,
|
||||
@@ -39,14 +42,22 @@ export const SmartContactModal = memo(function SmartContactModal() {
|
||||
|
||||
const areWeAdmin = conversation?.areWeAdmin ?? false;
|
||||
|
||||
const ourMembership = useMemo(() => {
|
||||
const contactMembership = useMemo(() => {
|
||||
// TODO: DESKTOP-9698
|
||||
return conversation?.memberships?.find(membership => {
|
||||
return membership.aci === contact.serviceId;
|
||||
});
|
||||
}, [conversation?.memberships, contact]);
|
||||
|
||||
const isMember = ourMembership != null;
|
||||
const isAdmin = ourMembership?.isAdmin ?? false;
|
||||
const isMember = contactMembership != null;
|
||||
const isAdmin = contactMembership?.isAdmin ?? false;
|
||||
const getMemberColors = useSelector(
|
||||
getCachedConversationMemberColorsSelector
|
||||
);
|
||||
const memberColors = getMemberColors(conversationId);
|
||||
const { labelEmoji: contactLabelEmoji, labelString: contactLabelString } =
|
||||
contactMembership || {};
|
||||
const contactNameColor = contactId ? memberColors.get(contactId) : undefined;
|
||||
|
||||
const {
|
||||
removeMemberFromGroup,
|
||||
@@ -81,6 +92,9 @@ export const SmartContactModal = memo(function SmartContactModal() {
|
||||
badges={badges}
|
||||
blockConversation={blockConversation}
|
||||
contact={contact}
|
||||
contactLabelEmoji={contactLabelEmoji}
|
||||
contactLabelString={contactLabelString}
|
||||
contactNameColor={contactNameColor}
|
||||
conversation={conversation}
|
||||
hasActiveCall={hasActiveCall}
|
||||
hasStories={hasStories}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { getActiveCallState } from '../selectors/calling.std.js';
|
||||
import {
|
||||
getAllComposableConversations,
|
||||
getCachedConversationMemberColorsSelector,
|
||||
getConversationByIdSelector,
|
||||
getConversationByServiceIdSelector,
|
||||
getPendingAvatarDownloadSelector,
|
||||
@@ -28,9 +29,10 @@ import {
|
||||
import {
|
||||
getAreWeASubscriber,
|
||||
getDefaultConversationColor,
|
||||
getItems,
|
||||
} from '../selectors/items.dom.js';
|
||||
import { getSelectedNavTab } from '../selectors/nav.preload.js';
|
||||
import { getIntl, getTheme } from '../selectors/user.std.js';
|
||||
import { getIntl, getTheme, getVersion } from '../selectors/user.std.js';
|
||||
import type { SmartChooseGroupMembersModalPropsType } from './ChooseGroupMembersModal.preload.js';
|
||||
import { SmartChooseGroupMembersModal } from './ChooseGroupMembersModal.preload.js';
|
||||
import type { SmartConfirmAdditionsModalPropsType } from './ConfirmAdditionsModal.dom.js';
|
||||
@@ -43,6 +45,7 @@ import { useGlobalModalActions } from '../ducks/globalModals.preload.js';
|
||||
import { isSignalConversation } from '../../util/isSignalConversation.dom.js';
|
||||
import { drop } from '../../util/drop.std.js';
|
||||
import { DataReader } from '../../sql/Client.preload.js';
|
||||
import { isFeaturedEnabledSelector } from '../../util/isFeatureEnabled.dom.js';
|
||||
|
||||
const { sortBy } = lodash;
|
||||
|
||||
@@ -93,6 +96,8 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
|
||||
const i18n = useSelector(getIntl);
|
||||
const theme = useSelector(getTheme);
|
||||
const activeCall = useSelector(getActiveCallState);
|
||||
const version = useSelector(getVersion);
|
||||
const items = useSelector(getItems);
|
||||
const allComposableConversations = useSelector(getAllComposableConversations);
|
||||
const areWeASubscriber = useSelector(getAreWeASubscriber);
|
||||
const badgesSelector = useSelector(getBadgesSelector);
|
||||
@@ -104,6 +109,9 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
|
||||
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||
const isPendingAvatarDownload = useSelector(getPendingAvatarDownloadSelector);
|
||||
const selectedNavTab = useSelector(getSelectedNavTab);
|
||||
const getCachedConversationMemberColors = useSelector(
|
||||
getCachedConversationMemberColorsSelector
|
||||
);
|
||||
|
||||
const {
|
||||
acceptConversation,
|
||||
@@ -155,6 +163,13 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
|
||||
const badges = badgesSelector(conversation.badges);
|
||||
const canAddNewMembers = conversation.canAddNewMembers ?? false;
|
||||
const canEditGroupInfo = conversation.canEditGroupInfo ?? false;
|
||||
const isEditMemberLabelEnabled = isFeaturedEnabledSelector({
|
||||
betaKey: 'desktop.groupMemberLabels.edit.beta',
|
||||
currentVersion: version,
|
||||
remoteConfig: items.remoteConfig,
|
||||
prodKey: 'desktop.groupMemberLabels.edit.prod',
|
||||
});
|
||||
|
||||
const groupsInCommon = getGroupsInCommonSorted(
|
||||
conversation,
|
||||
allComposableConversations
|
||||
@@ -169,6 +184,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
|
||||
const maxGroupSize = getGroupSizeHardLimit(1001);
|
||||
const maxRecommendedGroupSize = getGroupSizeRecommendedLimit(151);
|
||||
const userAvatarData = conversation.avatars ?? [];
|
||||
const memberColors = getCachedConversationMemberColors(conversationId);
|
||||
|
||||
const handleDeleteNicknameAndNote = useCallback(() => {
|
||||
updateNicknameAndNote(conversationId, { nickname: null, note: null });
|
||||
@@ -217,12 +233,14 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
|
||||
hasGroupLink={hasGroupLink}
|
||||
i18n={i18n}
|
||||
isAdmin={isAdmin}
|
||||
isEditMemberLabelEnabled={isEditMemberLabelEnabled}
|
||||
isGroup={isGroup}
|
||||
isSignalConversation={isSignalConversation(conversation)}
|
||||
leaveGroup={leaveGroup}
|
||||
hasMedia={hasMedia}
|
||||
maxGroupSize={maxGroupSize}
|
||||
maxRecommendedGroupSize={maxRecommendedGroupSize}
|
||||
memberColors={memberColors}
|
||||
memberships={memberships}
|
||||
onDeleteNicknameAndNote={handleDeleteNicknameAndNote}
|
||||
onOpenEditNicknameAndNoteModal={handleOpenEditNicknameAndNoteModal}
|
||||
|
||||
@@ -41,6 +41,7 @@ import { useReducedMotion } from '../../hooks/useReducedMotion.dom.js';
|
||||
import { itemStorage } from '../../textsecure/Storage.preload.js';
|
||||
import { SmartPinnedMessagesPanel } from './PinnedMessagesPanel.preload.js';
|
||||
import { SmartMiniPlayer } from './MiniPlayer.preload.js';
|
||||
import { SmartGroupMemberLabelEditor } from './SmartGroupMemberLabelEditor.preload.js';
|
||||
|
||||
const log = createLogger('ConversationPanel');
|
||||
|
||||
@@ -383,6 +384,10 @@ function PanelElement({
|
||||
return <SmartGroupLinkManagement conversationId={conversationId} />;
|
||||
}
|
||||
|
||||
if (panel.type === PanelType.GroupMemberLabelEditor) {
|
||||
return <SmartGroupMemberLabelEditor conversationId={conversationId} />;
|
||||
}
|
||||
|
||||
if (panel.type === PanelType.GroupPermissions) {
|
||||
return <SmartGroupV2Permissions conversationId={conversationId} />;
|
||||
}
|
||||
@@ -422,6 +427,7 @@ function getPanelKey(panel: PanelRenderType): string {
|
||||
case PanelType.GroupLinkManagement:
|
||||
case PanelType.GroupPermissions:
|
||||
case PanelType.GroupV1Members:
|
||||
case PanelType.GroupMemberLabelEditor:
|
||||
case PanelType.NotificationSettings:
|
||||
case PanelType.PinnedMessages:
|
||||
case PanelType.StickerManager:
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ConversationDetailsMembershipList } from '../../components/conversation
|
||||
import { assertDev } from '../../util/assert.std.js';
|
||||
import { getGroupMemberships } from '../../util/getGroupMemberships.dom.js';
|
||||
import {
|
||||
getCachedConversationMemberColorsSelector,
|
||||
getConversationByIdSelector,
|
||||
getConversationByServiceIdSelector,
|
||||
} from '../selectors/conversations.dom.js';
|
||||
@@ -31,6 +32,10 @@ export const SmartGV1Members = memo(function SmartGV1Members({
|
||||
const conversationByServiceIdSelector = useSelector(
|
||||
getConversationByServiceIdSelector
|
||||
);
|
||||
const getMemberColors = useSelector(
|
||||
getCachedConversationMemberColorsSelector
|
||||
);
|
||||
const memberColors = getMemberColors(conversationId);
|
||||
|
||||
const conversation = conversationSelector(conversationId);
|
||||
assertDev(
|
||||
@@ -50,6 +55,7 @@ export const SmartGV1Members = memo(function SmartGV1Members({
|
||||
i18n={i18n}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
maxShownMemberCount={32}
|
||||
memberColors={memberColors}
|
||||
memberships={memberships}
|
||||
showContactModal={showContactModal}
|
||||
theme={theme}
|
||||
|
||||
@@ -149,7 +149,7 @@ export const SmartGlobalModalContainer = memo(
|
||||
const hasSafetyNumberChangeModal = conversationsStoppingSend.length > 0;
|
||||
|
||||
const {
|
||||
aboutContactModalContactId,
|
||||
aboutContactModalState,
|
||||
addUserToAnotherGroupModalContactId,
|
||||
backfillFailureModalProps,
|
||||
callLinkAddNameModalRoomId,
|
||||
@@ -304,7 +304,7 @@ export const SmartGlobalModalContainer = memo(
|
||||
finishKeyTransparencyOnboarding={finishKeyTransparencyOnboarding}
|
||||
hideTapToViewNotAvailableModal={hideTapToViewNotAvailableModal}
|
||||
i18n={i18n}
|
||||
isAboutContactModalVisible={aboutContactModalContactId != null}
|
||||
isAboutContactModalVisible={aboutContactModalState != null}
|
||||
isKeyTransparencyErrorVisible={isKeyTransparencyErrorVisible}
|
||||
isKeyTransparencyOnboardingVisible={isKeyTransparencyOnboardingVisible}
|
||||
isProfileNameWarningModalVisible={isProfileNameWarningModalVisible}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { SafetyNumberViewer } from '../../components/SafetyNumberViewer.dom.js';
|
||||
import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog.dom.js';
|
||||
import { getContactSafetyNumberSelector } from '../selectors/safetyNumber.std.js';
|
||||
import { getConversationSelector } from '../selectors/conversations.dom.js';
|
||||
import { getIntl } from '../selectors/user.std.js';
|
||||
import { getIntl, getVersion } from '../selectors/user.std.js';
|
||||
import { getItems } from '../selectors/items.dom.js';
|
||||
import { useSafetyNumberActions } from '../ducks/safetyNumber.preload.js';
|
||||
import { keyTransparency } from '../../services/keyTransparency.preload.js';
|
||||
@@ -31,7 +31,7 @@ export const SmartSafetyNumberViewer = memo(function SmartSafetyNumberViewer({
|
||||
const contact = conversationSelector(contactID);
|
||||
const items = useSelector(getItems);
|
||||
|
||||
const version = window.SignalContext.getVersion();
|
||||
const version = useSelector(getVersion);
|
||||
const isKeyTransparencyEnabled = isFeaturedEnabledSelector({
|
||||
betaKey: 'desktop.keyTransparency.beta',
|
||||
prodKey: 'desktop.keyTransparency.prod',
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { GroupMemberLabelEditor } from '../../components/conversation/conversation-details/GroupMemberLabelEditor.dom.js';
|
||||
import { getConversationSelector } from '../selectors/conversations.dom.js';
|
||||
import { getIntl, getTheme, getUser } from '../selectors/user.std.js';
|
||||
import { useConversationsActions } from '../ducks/conversations.preload.js';
|
||||
import { createLogger } from '../../logging/log.std.js';
|
||||
|
||||
const log = createLogger('SmartGroupMemberLabelEditor');
|
||||
|
||||
export type SmartGroupMemberLabelEditorProps = Readonly<{
|
||||
conversationId: string;
|
||||
}>;
|
||||
|
||||
export const SmartGroupMemberLabelEditor = memo(
|
||||
function SmartGroupMemberLabelEditor({
|
||||
conversationId,
|
||||
}: SmartGroupMemberLabelEditorProps) {
|
||||
const i18n = useSelector(getIntl);
|
||||
const theme = useSelector(getTheme);
|
||||
const user = useSelector(getUser);
|
||||
|
||||
const conversationSelector = useSelector(getConversationSelector);
|
||||
const conversation = conversationSelector(conversationId);
|
||||
const { updateGroupMemberLabel, popPanelForConversation } =
|
||||
useConversationsActions();
|
||||
|
||||
const { ourAci } = user;
|
||||
// TODO: DESKTOP-9698
|
||||
const ourMembership = conversation.memberships?.find(
|
||||
membership => membership?.aci === ourAci
|
||||
);
|
||||
if (!ourMembership) {
|
||||
log.warn('User was not found in group, leaving this pane!');
|
||||
popPanelForConversation();
|
||||
return null;
|
||||
}
|
||||
const { labelEmoji: existingLabelEmoji, labelString: existingLabelString } =
|
||||
ourMembership;
|
||||
|
||||
return (
|
||||
<GroupMemberLabelEditor
|
||||
i18n={i18n}
|
||||
conversation={conversation}
|
||||
existingLabelEmoji={existingLabelEmoji}
|
||||
existingLabelString={existingLabelString}
|
||||
popPanelForConversation={popPanelForConversation}
|
||||
theme={theme}
|
||||
updateGroupMemberLabel={updateGroupMemberLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -71,6 +71,8 @@ export function getDefaultGroup(
|
||||
const memberships = Array.from(Array(casual.integer(1, 20)), () => ({
|
||||
aci: generateAci(),
|
||||
isAdmin: Boolean(casual.coin_flip),
|
||||
labelEmoji: undefined,
|
||||
labelString: undefined,
|
||||
}));
|
||||
|
||||
return {
|
||||
|
||||
@@ -14,7 +14,12 @@ const CONFIG_KEY = 'global.calling.maxGroupCallRingSize';
|
||||
|
||||
describe('isConversationTooBigToRing', () => {
|
||||
const fakeMemberships = (count: number) =>
|
||||
times(count, () => ({ aci: generateAci(), isAdmin: false }));
|
||||
times(count, () => ({
|
||||
aci: generateAci(),
|
||||
isAdmin: false,
|
||||
labelEmoji: undefined,
|
||||
labelString: undefined,
|
||||
}));
|
||||
|
||||
it('returns false if there are no memberships (i.e., for a direct conversation)', async () => {
|
||||
await updateRemoteConfig([]);
|
||||
|
||||
@@ -56,6 +56,8 @@ describe('getGroupMemberships', () => {
|
||||
{
|
||||
aci: generateAci(),
|
||||
isAdmin: true,
|
||||
labelEmoji: undefined,
|
||||
labelString: undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -74,6 +76,8 @@ describe('getGroupMemberships', () => {
|
||||
{
|
||||
aci: normalizeAci(unregisteredConversation.serviceId, 'test'),
|
||||
isAdmin: true,
|
||||
labelEmoji: undefined,
|
||||
labelString: 'Task Wrangler',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -87,6 +91,8 @@ describe('getGroupMemberships', () => {
|
||||
assert.deepEqual(result[0], {
|
||||
isAdmin: true,
|
||||
member: unregisteredConversation,
|
||||
labelEmoji: undefined,
|
||||
labelString: 'Task Wrangler',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,10 +102,14 @@ describe('getGroupMemberships', () => {
|
||||
{
|
||||
aci: normalizeAci(normalConversation2.serviceId, 'test'),
|
||||
isAdmin: false,
|
||||
labelEmoji: undefined,
|
||||
labelString: undefined,
|
||||
},
|
||||
{
|
||||
aci: normalizeAci(normalConversation1.serviceId, 'test'),
|
||||
isAdmin: true,
|
||||
labelEmoji: '✅',
|
||||
labelString: 'Task Wrangler',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -113,10 +123,14 @@ describe('getGroupMemberships', () => {
|
||||
assert.deepEqual(result[0], {
|
||||
isAdmin: false,
|
||||
member: normalConversation2,
|
||||
labelEmoji: undefined,
|
||||
labelString: undefined,
|
||||
});
|
||||
assert.deepEqual(result[1], {
|
||||
isAdmin: true,
|
||||
member: normalConversation1,
|
||||
labelEmoji: '✅',
|
||||
labelString: 'Task Wrangler',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ export enum PanelType {
|
||||
GroupLinkManagement = 'GroupLinkManagement',
|
||||
GroupPermissions = 'GroupPermissions',
|
||||
GroupV1Members = 'GroupV1Members',
|
||||
GroupMemberLabelEditor = 'GroupMemberLabelEditor',
|
||||
MessageDetails = 'MessageDetails',
|
||||
NotificationSettings = 'NotificationSettings',
|
||||
PinnedMessages = 'PinnedMessages',
|
||||
@@ -34,6 +35,7 @@ export type PanelRequestType = ReadonlyDeep<
|
||||
| { type: PanelType.GroupLinkManagement }
|
||||
| { type: PanelType.GroupPermissions }
|
||||
| { type: PanelType.GroupV1Members }
|
||||
| { type: PanelType.GroupMemberLabelEditor }
|
||||
| { type: PanelType.MessageDetails; args: { messageId: string } }
|
||||
| { type: PanelType.NotificationSettings }
|
||||
| { type: PanelType.PinnedMessages }
|
||||
@@ -54,6 +56,7 @@ export type PanelRenderType = ReadonlyDeep<
|
||||
| { type: PanelType.GroupLinkManagement }
|
||||
| { type: PanelType.GroupPermissions }
|
||||
| { type: PanelType.GroupV1Members }
|
||||
| { type: PanelType.GroupMemberLabelEditor }
|
||||
| {
|
||||
type: PanelType.MessageDetails;
|
||||
args: { message: ReadonlyMessageAttributesType };
|
||||
|
||||
@@ -35,6 +35,10 @@ export function getConversationTitleForPanelType(
|
||||
return i18n('icu:ConversationDetails--requests-and-invites');
|
||||
}
|
||||
|
||||
if (panelType === PanelType.GroupMemberLabelEditor) {
|
||||
return i18n('icu:ConversationDetails--member-label');
|
||||
}
|
||||
|
||||
if (panelType === PanelType.GroupLinkManagement) {
|
||||
return i18n('icu:ConversationDetails--group-link');
|
||||
}
|
||||
|
||||
@@ -39,7 +39,15 @@ export const getGroupMemberships = (
|
||||
if (!member) {
|
||||
return result;
|
||||
}
|
||||
return [...result, { isAdmin: membership.isAdmin, member }];
|
||||
return [
|
||||
...result,
|
||||
{
|
||||
isAdmin: membership.isAdmin,
|
||||
labelEmoji: membership.labelEmoji,
|
||||
labelString: membership.labelString,
|
||||
member,
|
||||
},
|
||||
];
|
||||
},
|
||||
[]
|
||||
),
|
||||
|
||||
@@ -177,6 +177,8 @@ export function getMemberships(
|
||||
): ReadonlyArray<{
|
||||
aci: AciString;
|
||||
isAdmin: boolean;
|
||||
labelEmoji: string | undefined;
|
||||
labelString: string | undefined;
|
||||
}> {
|
||||
if (!isGroupV2(conversationAttrs)) {
|
||||
return EMPTY_ARRAY;
|
||||
@@ -186,6 +188,8 @@ export function getMemberships(
|
||||
return members.map(member => ({
|
||||
isAdmin: member.role === Proto.Member.Role.ADMINISTRATOR,
|
||||
aci: member.aci,
|
||||
labelEmoji: member.labelEmoji,
|
||||
labelString: member.labelString,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user