@@ -89,14 +117,16 @@ export function GroupMemberLabelEditor({
}
- maxLengthCount={24}
- maxByteCount={96}
+ maxLengthCount={STRING_GRAPHEME_LIMIT}
+ maxByteCount={STRING_BYTE_LIMIT}
moduleClassName="GroupMemberLabelEditor"
onChange={value => {
if (!value) {
setLabelEmoji(undefined);
}
- setLabelString(value);
+
+ // Remove trailing/leading whitespace, replace all whitespace with basic space
+ setLabelString(value.replace(/\s/g, ' '));
}}
ref={undefined}
placeholder={i18n(
@@ -106,12 +136,86 @@ export function GroupMemberLabelEditor({
whenToShowRemainingCount={20}
/>
-
+
{i18n('icu:ConversationDetails--member-label--description')}
+
+ {i18n('icu:ConversationDetails--member-label--preview')}
+
+
+
}
+ doubleCheckMissingQuoteReference={noop}
+ messageExpanded={noop}
+ checkForAccount={noop}
+ startConversation={noop}
+ showConversation={noop}
+ openGiftBadge={noop}
+ pushPanelForConversation={noop}
+ retryMessageSend={noop}
+ sendPollVote={noop}
+ endPoll={noop}
+ showContactModal={noop}
+ showSpoiler={noop}
+ cancelAttachmentDownload={noop}
+ kickOffAttachmentDownload={noop}
+ markAttachmentAsCorrupted={noop}
+ saveAttachment={noop}
+ saveAttachments={noop}
+ showLightbox={noop}
+ showLightboxForViewOnceMedia={noop}
+ scrollToQuotedMessage={noop}
+ showAttachmentDownloadStillInProgressToast={noop}
+ showExpiredIncomingTapToViewToast={noop}
+ showExpiredOutgoingTapToViewToast={noop}
+ showMediaNoLongerAvailableToast={noop}
+ showTapToViewNotAvailableModal={noop}
+ viewStory={noop}
+ onToggleSelect={noop}
+ onReplyToMessage={noop}
+ />
+
-
-
+
{
setIsSaving(true);
updateGroupMemberLabel(
{
- conversationId: conversation.id,
+ conversationId: group.id,
labelEmoji,
- labelString,
+ labelString: labelString?.trim(),
},
{
onSuccess() {
@@ -141,7 +245,7 @@ export function GroupMemberLabelEditor({
popPanelForConversation();
},
onFailure() {
- // TODO: DESKTOP-9698
+ // TODO: DESKTOP-9710
},
}
);
diff --git a/ts/groups.preload.ts b/ts/groups.preload.ts
index fba44eef9f..6db53cd304 100644
--- a/ts/groups.preload.ts
+++ b/ts/groups.preload.ts
@@ -127,6 +127,12 @@ import {
isTrustedContact,
} from './util/isConversationAccepted.preload.js';
import { itemStorage } from './textsecure/Storage.preload.js';
+import {
+ EMOJI_OUTGOING_BYTE_LIMIT,
+ SERVER_EMOJI_BYTE_LIMIT,
+ SERVER_STRING_BYTE_LIMIT,
+} from './types/GroupMemberLabels.std.js';
+import { getConversationIdForLogging } from './util/idForLogging.preload.js';
const { compact, difference, flatten, fromPairs, isNumber, omit, values } =
lodash;
@@ -1280,11 +1286,10 @@ export function buildModifyMemberLabelChange({
labelString: string | undefined;
}): Proto.GroupChange.Actions {
const actions = new Proto.GroupChange.Actions();
+ const logId = `buildModifyMemberLabelChange(${getConversationIdForLogging(group)})`;
if (!group.secretParams) {
- throw new Error(
- 'buildModifyMemberLabelChange: group was missing secretParams!'
- );
+ throw new Error(`${logId}: group was missing secretParams!`);
}
const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams);
@@ -1293,16 +1298,38 @@ export function buildModifyMemberLabelChange({
const modifyLabel = new Proto.GroupChange.Actions.ModifyMemberLabelAction();
modifyLabel.userId = userIdCipherText;
if (labelEmoji) {
+ const labelEmojiBytes = Bytes.fromString(labelEmoji);
+
+ if (labelEmojiBytes.byteLength > EMOJI_OUTGOING_BYTE_LIMIT) {
+ throw new Error(
+ `${logId}: plaintext label emoji length (${labelEmojiBytes.byteLength}) is larger than limit (${EMOJI_OUTGOING_BYTE_LIMIT})!`
+ );
+ }
+
modifyLabel.labelEmoji = encryptGroupBlob(
clientZkGroupCipher,
- Bytes.fromString(labelEmoji)
+ labelEmojiBytes
);
+ if (modifyLabel.labelEmoji.byteLength > SERVER_EMOJI_BYTE_LIMIT) {
+ throw new Error(
+ `${logId}: encrypted label emoji length (${modifyLabel.labelEmoji.length}) is larger than limit (${SERVER_EMOJI_BYTE_LIMIT})!`
+ );
+ }
}
if (labelString) {
modifyLabel.labelString = encryptGroupBlob(
clientZkGroupCipher,
Bytes.fromString(labelString)
);
+ if (modifyLabel.labelString.byteLength > SERVER_STRING_BYTE_LIMIT) {
+ throw new Error(
+ `${logId} encrypted label string length (${modifyLabel.labelString.length}) is larger than limit (${SERVER_STRING_BYTE_LIMIT})!`
+ );
+ }
+ }
+
+ if (modifyLabel.labelEmoji && !modifyLabel.labelString) {
+ throw new Error(`${logId} labelEmoji was provided, but not labelString!`);
}
actions.version = (group.revision || 0) + 1;
diff --git a/ts/services/backups/export.preload.ts b/ts/services/backups/export.preload.ts
index d1efa9587c..bf6ca2c964 100644
--- a/ts/services/backups/export.preload.ts
+++ b/ts/services/backups/export.preload.ts
@@ -1340,9 +1340,11 @@ export class BackupExportStream extends Readable {
version: convo.revision || 0,
members: convo.membersV2?.map(member => {
return {
- userId: this.#aciToBytes(member.aci),
- role: member.role,
joinedAtVersion: member.joinedAtVersion,
+ labelEmoji: member.labelEmoji,
+ labelString: member.labelString,
+ role: member.role,
+ userId: this.#aciToBytes(member.aci),
};
}),
membersPendingProfileKey: convo.pendingMembersV2?.map(member => {
diff --git a/ts/services/backups/import.preload.ts b/ts/services/backups/import.preload.ts
index 6b5e787bf7..3036158dba 100644
--- a/ts/services/backups/import.preload.ts
+++ b/ts/services/backups/import.preload.ts
@@ -1241,18 +1241,22 @@ export class BackupImportStream extends Writable {
SignalService.AccessControl.AccessRequired.UNKNOWN,
}
: undefined,
- membersV2: members?.map(({ userId, role, joinedAtVersion }) => {
- strictAssert(Bytes.isNotEmpty(userId), 'Empty gv2 member userId');
+ membersV2: members?.map(
+ ({ joinedAtVersion, labelEmoji, labelString, role, userId }) => {
+ strictAssert(Bytes.isNotEmpty(userId), 'Empty gv2 member userId');
- // Note that we deliberately ignore profile key since it has to be
- // in the Contact frame
+ // Note that we deliberately ignore profile key since it has to be
+ // in the Contact frame
- return {
- aci: fromAciObject(Aci.fromUuidBytes(userId)),
- role: dropNull(role) ?? SignalService.Member.Role.UNKNOWN,
- joinedAtVersion: dropNull(joinedAtVersion) ?? 0,
- };
- }),
+ return {
+ aci: fromAciObject(Aci.fromUuidBytes(userId)),
+ joinedAtVersion: dropNull(joinedAtVersion) ?? 0,
+ labelEmoji: dropNull(labelEmoji),
+ labelString: dropNull(labelString),
+ role: dropNull(role) ?? SignalService.Member.Role.UNKNOWN,
+ };
+ }
+ ),
pendingMembersV2: membersPendingProfileKey?.map(
({ member, addedByUserId, timestamp }) => {
strictAssert(member != null, 'Missing gv2 pending member');
diff --git a/ts/state/ducks/conversations.preload.ts b/ts/state/ducks/conversations.preload.ts
index c0611f2d74..323f654ece 100644
--- a/ts/state/ducks/conversations.preload.ts
+++ b/ts/state/ducks/conversations.preload.ts
@@ -325,6 +325,13 @@ export type ConversationRemovalStage = ReadonlyDeep<
'justNotification' | 'messageRequest'
>;
+export type MembershipType = ReadonlyDeep<{
+ aci: AciString;
+ isAdmin: boolean;
+ labelEmoji: string | undefined;
+ labelString: string | undefined;
+}>;
+
export type ConversationType = ReadonlyDeep<
{
id: string;
@@ -393,12 +400,7 @@ export type ConversationType = ReadonlyDeep<
announcementsOnly?: boolean;
announcementsOnlyReady?: boolean;
expireTimer?: DurationInSeconds;
- memberships?: ReadonlyArray<{
- aci: AciString;
- isAdmin: boolean;
- labelEmoji: string | undefined;
- labelString: string | undefined;
- }>;
+ memberships?: ReadonlyArray;
pendingMemberships?: ReadonlyArray<{
serviceId: ServiceIdString;
addedByUserId?: AciString;
@@ -4640,9 +4642,8 @@ function addMembersToGroup(
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type ActionCreator) => any> = ReadonlyDeep<
- (...params: Parameters) => void
->;
+export type ActionCreator) => any> =
+ ReadonlyDeep<(...params: Parameters) => void>;
export type UpdateGroupAttributesType = ReadonlyDeep<
ActionCreator
diff --git a/ts/state/ducks/globalModals.preload.ts b/ts/state/ducks/globalModals.preload.ts
index e13b565f33..df9b077ecd 100644
--- a/ts/state/ducks/globalModals.preload.ts
+++ b/ts/state/ducks/globalModals.preload.ts
@@ -11,6 +11,7 @@ import type {
ReadonlyMessageAttributesType,
} from '../../model-types.d.ts';
import type {
+ ActionCreator,
MessageChangedActionType,
MessageDeletedActionType,
MessageExpiredActionType,
@@ -114,6 +115,10 @@ export type CallQualitySurveyPropsType = ReadonlyDeep<{
callType: CallQualitySurvey.CallType;
}>;
+export type GroupMemberLabelInfoPropsType = ReadonlyDeep<{
+ conversationId: string;
+}>;
+
export type GlobalModalsStateType = ReadonlyDeep<{
addUserToAnotherGroupModalContactId?: string;
aboutContactModalState?: ContactModalStateType;
@@ -139,6 +144,7 @@ export type GlobalModalsStateType = ReadonlyDeep<{
};
forwardMessagesProps?: ForwardMessagesPropsType;
gv2MigrationProps?: MigrateToGV2PropsType;
+ groupMemberLabelInfoModalState?: GroupMemberLabelInfoPropsType;
hasConfirmationModal: boolean;
isProfileNameWarningModalVisible: boolean;
profileNameWarningModalConversationType?: string;
@@ -227,6 +233,8 @@ const CLOSE_DEBUG_LOG_ERROR_MODAL = 'globalModals/CLOSE_DEBUG_LOG_ERROR_MODAL';
const SHOW_DEBUG_LOG_ERROR_MODAL = 'globalModals/SHOW_DEBUG_LOG_ERROR_MODAL';
const TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL =
'globalModals/TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL';
+const TOGGLE_GROUP_MEMBER_LABEL_INFO_MODAL =
+ 'globalModals/TOGGLE_GROUP_MEMBER_LABEL_INFO_MODAL';
const TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION =
'globalModals/TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION';
const CLOSE_SHORTCUT_GUIDE_MODAL = 'globalModals/CLOSE_SHORTCUT_GUIDE_MODAL';
@@ -509,6 +517,11 @@ type ToggleEditNicknameAndNoteModalActionType = ReadonlyDeep<{
payload: EditNicknameAndNoteModalPropsType | null;
}>;
+type ToggleGroupMemberLabelInfoModalActionType = ReadonlyDeep<{
+ type: typeof TOGGLE_GROUP_MEMBER_LABEL_INFO_MODAL;
+ payload: GroupMemberLabelInfoPropsType | undefined;
+}>;
+
type ToggleMessageRequestActionsConfirmationActionType = ReadonlyDeep<{
type: typeof TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION;
payload: MessageRequestActionsConfirmationPropsType | null;
@@ -591,6 +604,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
| ToggleDraftGifMessageSendModalActionType
| ToggleEditNicknameAndNoteModalActionType
| ToggleForwardMessagesModalActionType
+ | ToggleGroupMemberLabelInfoModalActionType
| ToggleMessageRequestActionsConfirmationActionType
| ToggleNotePreviewModalActionType
| ToggleProfileNameWarningModalActionType
@@ -654,6 +668,7 @@ export const actions = {
toggleDraftGifMessageSendModal,
toggleEditNicknameAndNoteModal,
toggleForwardMessagesModal,
+ toggleGroupMemberLabelInfoModal,
toggleMessageRequestActionsConfirmation,
toggleNotePreviewModal,
toggleProfileNameWarningModal,
@@ -1349,6 +1364,18 @@ function toggleEditNicknameAndNoteModal(
};
}
+export type ToggleGroupMemberLabelInfoModalType = ReadonlyDeep<
+ ActionCreator
+>;
+function toggleGroupMemberLabelInfoModal(
+ payload: GroupMemberLabelInfoPropsType | undefined
+): ToggleGroupMemberLabelInfoModalActionType {
+ return {
+ type: TOGGLE_GROUP_MEMBER_LABEL_INFO_MODAL,
+ payload,
+ };
+}
+
function toggleMessageRequestActionsConfirmation(
payload: {
conversationId: string;
@@ -1818,6 +1845,13 @@ export function reducer(
};
}
+ if (action.type === TOGGLE_GROUP_MEMBER_LABEL_INFO_MODAL) {
+ return {
+ ...state,
+ groupMemberLabelInfoModalState: action.payload,
+ };
+ }
+
if (action.type === TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION) {
return {
...state,
diff --git a/ts/state/selectors/globalModals.std.ts b/ts/state/selectors/globalModals.std.ts
index c380d99dfa..68f53d41b1 100644
--- a/ts/state/selectors/globalModals.std.ts
+++ b/ts/state/selectors/globalModals.std.ts
@@ -55,6 +55,11 @@ export const getAboutContactModalState = createSelector(
({ aboutContactModalState }) => aboutContactModalState
);
+export const getGroupMemberLabelInfoModalState = createSelector(
+ getGlobalModalsState,
+ ({ groupMemberLabelInfoModalState }) => groupMemberLabelInfoModalState
+);
+
export const getContactModalState = createSelector(
getGlobalModalsState,
({ contactModalState }) => contactModalState
diff --git a/ts/state/selectors/message.preload.ts b/ts/state/selectors/message.preload.ts
index 14e72b5f9e..c80b67ae71 100644
--- a/ts/state/selectors/message.preload.ts
+++ b/ts/state/selectors/message.preload.ts
@@ -905,7 +905,6 @@ export const getPropsForMessage = (
});
const contactNameColor = getContactNameColor(contactNameColors, authorId);
const sourceServiceId = getSourceServiceId(message, ourAci);
- // TODO: DESKTOP-9698
const sourceMember = conversation.memberships?.find(
membership => membership.aci === sourceServiceId
);
diff --git a/ts/state/smart/AboutContactModal.preload.tsx b/ts/state/smart/AboutContactModal.preload.tsx
index dda7892456..93dd8f38f0 100644
--- a/ts/state/smart/AboutContactModal.preload.tsx
+++ b/ts/state/smart/AboutContactModal.preload.tsx
@@ -18,9 +18,12 @@ 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';
+import { getCanAddLabel } from '../../types/GroupMemberLabels.std.js';
+import { createLogger } from '../../logging/log.std.js';
+
+const log = createLogger('SmartAboutContactModal');
function isFromOrAddedByTrustedContact(
conversation: ConversationType
@@ -54,6 +57,10 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() {
remoteConfig: items.remoteConfig,
prodKey: 'desktop.groupMemberLabels.edit.prod',
});
+ // TODO: DESKTOP-9711
+ log.info(
+ `Not using feature flag of ${isEditMemberLabelEnabled}; hardcoding to false`
+ );
const sharedGroupNames = useSharedGroupNamesOnMount(contactId ?? '');
@@ -67,17 +74,12 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() {
);
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 canAddLabel = getCanAddLabel(conversation, contactMembership);
const {
toggleAboutContactModal,
@@ -105,7 +107,7 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() {
contactLabelString={contactLabelString}
contactNameColor={contactNameColor}
fromOrAddedByTrustedContact={isFromOrAddedByTrustedContact(contact)}
- isEditMemberLabelEnabled={isEditMemberLabelEnabled}
+ isEditMemberLabelEnabled={false}
isSignalConnection={isSignalConnection(contact)}
onClose={toggleAboutContactModal}
onOpenNotePreviewModal={handleOpenNotePreviewModal}
diff --git a/ts/state/smart/ContactModal.preload.tsx b/ts/state/smart/ContactModal.preload.tsx
index fafd15d71e..44e084631c 100644
--- a/ts/state/smart/ContactModal.preload.tsx
+++ b/ts/state/smart/ContactModal.preload.tsx
@@ -43,7 +43,6 @@ export const SmartContactModal = memo(function SmartContactModal() {
const areWeAdmin = conversation?.areWeAdmin ?? false;
const contactMembership = useMemo(() => {
- // TODO: DESKTOP-9698
return conversation?.memberships?.find(membership => {
return membership.aci === contact.serviceId;
});
@@ -68,11 +67,12 @@ export const SmartContactModal = memo(function SmartContactModal() {
} = useConversationsActions();
const { viewUserStories } = useStoriesActions();
const {
+ hideContactModal,
toggleAboutContactModal,
toggleAddUserToAnotherGroupModal,
- toggleSafetyNumberModal,
- hideContactModal,
toggleEditNicknameAndNoteModal,
+ toggleGroupMemberLabelInfoModal,
+ toggleSafetyNumberModal,
} = useGlobalModalActions();
const {
onOutgoingVideoCallInConversation,
@@ -113,6 +113,7 @@ export const SmartContactModal = memo(function SmartContactModal() {
toggleAboutContactModal={toggleAboutContactModal}
toggleAddUserToAnotherGroupModal={toggleAddUserToAnotherGroupModal}
toggleAdmin={toggleAdmin}
+ toggleGroupMemberLabelInfoModal={toggleGroupMemberLabelInfoModal}
togglePip={togglePip}
toggleSafetyNumberModal={toggleSafetyNumberModal}
viewUserStories={viewUserStories}
diff --git a/ts/state/smart/ConversationDetails.preload.tsx b/ts/state/smart/ConversationDetails.preload.tsx
index e14b6ade16..53a40b9ed6 100644
--- a/ts/state/smart/ConversationDetails.preload.tsx
+++ b/ts/state/smart/ConversationDetails.preload.tsx
@@ -32,7 +32,12 @@ import {
getItems,
} from '../selectors/items.dom.js';
import { getSelectedNavTab } from '../selectors/nav.preload.js';
-import { getIntl, getTheme, getVersion } from '../selectors/user.std.js';
+import {
+ getIntl,
+ getTheme,
+ getUserACI,
+ 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';
@@ -46,6 +51,7 @@ 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';
+import { getCanAddLabel } from '../../types/GroupMemberLabels.std.js';
const { sortBy } = lodash;
@@ -95,6 +101,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
}: SmartConversationDetailsProps) {
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
+ const ourAci = useSelector(getUserACI);
const activeCall = useSelector(getActiveCallState);
const version = useSelector(getVersion);
const items = useSelector(getItems);
@@ -186,6 +193,11 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
const userAvatarData = conversation.avatars ?? [];
const memberColors = getCachedConversationMemberColors(conversationId);
+ const ourMembership = conversation.memberships?.find(
+ membership => membership?.aci === ourAci
+ );
+ const canAddLabel = getCanAddLabel(conversation, ourMembership);
+
const handleDeleteNicknameAndNote = useCallback(() => {
updateNicknameAndNote(conversationId, { nickname: null, note: null });
}, [conversationId, updateNicknameAndNote]);
@@ -222,6 +234,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
badges={badges}
blockConversation={blockConversation}
callHistoryGroup={callHistoryGroup}
+ canAddLabel={canAddLabel}
canAddNewMembers={canAddNewMembers}
canEditGroupInfo={canEditGroupInfo}
conversation={conversationWithColorAttributes}
diff --git a/ts/state/smart/GlobalModalContainer.preload.tsx b/ts/state/smart/GlobalModalContainer.preload.tsx
index e0c6c17d65..9c21ebb7b6 100644
--- a/ts/state/smart/GlobalModalContainer.preload.tsx
+++ b/ts/state/smart/GlobalModalContainer.preload.tsx
@@ -41,6 +41,7 @@ import {
shouldShowLocalBackupWorkflow,
} from '../selectors/backups.std.js';
import { SmartPinMessageDialog } from './PinMessageDialog.preload.js';
+import { SmartGroupMemberLabelInfoModal } from './GroupMemberLabelInfoModal.preload.js';
function renderCallLinkAddNameModal(): React.JSX.Element {
return ;
@@ -94,6 +95,10 @@ function renderForwardMessagesModal(): React.JSX.Element {
return ;
}
+function renderGroupMemberLabelInfoModal(): React.JSX.Element {
+ return ;
+}
+
function renderKeyTransparencyErrorDialog(): React.JSX.Element {
return ;
}
@@ -166,6 +171,7 @@ export const SmartGlobalModalContainer = memo(
editNicknameAndNoteModalProps,
errorModalProps,
forwardMessagesProps,
+ groupMemberLabelInfoModalState,
lowDiskSpaceBackupImportModal,
mediaPermissionsModalProps,
messageRequestActionsConfirmationProps,
@@ -283,6 +289,7 @@ export const SmartGlobalModalContainer = memo(
deleteMessagesProps={deleteMessagesProps}
draftGifMessageSendModalProps={draftGifMessageSendModalProps}
forwardMessagesProps={forwardMessagesProps}
+ groupMemberLabelInfoModalState={groupMemberLabelInfoModalState}
hideCriticalIdlePrimaryDeviceModal={hideCriticalIdlePrimaryDeviceModal}
hideLowDiskSpaceBackupImportModal={hideLowDiskSpaceBackupImportModal}
lowDiskSpaceBackupImportModal={lowDiskSpaceBackupImportModal}
@@ -328,6 +335,7 @@ export const SmartGlobalModalContainer = memo(
renderDeleteMessagesModal={renderDeleteMessagesModal}
renderDraftGifMessageSendModal={renderDraftGifMessageSendModal}
renderForwardMessagesModal={renderForwardMessagesModal}
+ renderGroupMemberLabelInfoModal={renderGroupMemberLabelInfoModal}
renderKeyTransparencyErrorDialog={renderKeyTransparencyErrorDialog}
renderMessageRequestActionsConfirmation={
renderMessageRequestActionsConfirmation
diff --git a/ts/state/smart/GroupMemberLabelInfoModal.preload.tsx b/ts/state/smart/GroupMemberLabelInfoModal.preload.tsx
new file mode 100644
index 0000000000..945e70dfbb
--- /dev/null
+++ b/ts/state/smart/GroupMemberLabelInfoModal.preload.tsx
@@ -0,0 +1,63 @@
+// Copyright 2024 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+import React, { memo } from 'react';
+import { useSelector } from 'react-redux';
+
+import { GroupMemberLabelInfoModal } from '../../components/GroupMemberLabelInfoModal.dom.js';
+import { getIntl, getUser, getVersion } from '../selectors/user.std.js';
+import { getGroupMemberLabelInfoModalState } from '../selectors/globalModals.std.js';
+import { getConversationSelector } from '../selectors/conversations.dom.js';
+import { useGlobalModalActions } from '../ducks/globalModals.preload.js';
+import { getItems } from '../selectors/items.dom.js';
+import { isFeaturedEnabledSelector } from '../../util/isFeatureEnabled.dom.js';
+import { getCanAddLabel } from '../../types/GroupMemberLabels.std.js';
+import { createLogger } from '../../logging/log.std.js';
+
+const log = createLogger('SmartGroupMemberLabelInfoModal');
+
+export const SmartGroupMemberLabelInfoModal = memo(
+ function SmartGroupMemberLabelInfoModal() {
+ const i18n = useSelector(getIntl);
+ const user = useSelector(getUser);
+ const version = useSelector(getVersion);
+ const items = useSelector(getItems);
+ const { conversationId } =
+ useSelector(getGroupMemberLabelInfoModalState) ?? {};
+ const getConversation = useSelector(getConversationSelector);
+
+ const isEditMemberLabelEnabled = isFeaturedEnabledSelector({
+ betaKey: 'desktop.groupMemberLabels.edit.beta',
+ currentVersion: version,
+ remoteConfig: items.remoteConfig,
+ prodKey: 'desktop.groupMemberLabels.edit.prod',
+ });
+ // TODO: DESKTOP-9711
+ log.info(
+ `Not using feature flag of ${isEditMemberLabelEnabled}; hardcoding to false`
+ );
+
+ const conversation = getConversation(conversationId);
+
+ const contactMembership = conversation.memberships?.find(
+ membership => user.ourAci && membership.aci === user.ourAci
+ );
+ const hasLabel = Boolean(contactMembership?.labelString);
+ const canAddLabel = getCanAddLabel(conversation, contactMembership);
+
+ const { toggleGroupMemberLabelInfoModal } = useGlobalModalActions();
+
+ return (
+ toggleGroupMemberLabelInfoModal(undefined)}
+ showEditMemberLabelScreen={() => {
+ // TODO: DESKTOP-9711
+ throw new Error('Not yet implemented');
+ }}
+ />
+ );
+ }
+);
diff --git a/ts/state/smart/SmartGroupMemberLabelEditor.preload.tsx b/ts/state/smart/SmartGroupMemberLabelEditor.preload.tsx
index 20f550956a..eef34eb3b9 100644
--- a/ts/state/smart/SmartGroupMemberLabelEditor.preload.tsx
+++ b/ts/state/smart/SmartGroupMemberLabelEditor.preload.tsx
@@ -1,12 +1,18 @@
// 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 {
+ getCachedConversationMemberColorsSelector,
+ 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';
+import { getPreferredBadgeSelector } from '../selectors/badges.preload.js';
const log = createLogger('SmartGroupMemberLabelEditor');
@@ -24,11 +30,18 @@ export const SmartGroupMemberLabelEditor = memo(
const conversationSelector = useSelector(getConversationSelector);
const conversation = conversationSelector(conversationId);
+ const me = conversationSelector(user.ourAci);
+
const { updateGroupMemberLabel, popPanelForConversation } =
useConversationsActions();
+ const getMemberColors = useSelector(
+ getCachedConversationMemberColorsSelector
+ );
+ const memberColors = getMemberColors(conversationId);
+ const ourColor = memberColors?.get(me.id);
+ const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const { ourAci } = user;
- // TODO: DESKTOP-9698
const ourMembership = conversation.memberships?.find(
membership => membership?.aci === ourAci
);
@@ -42,10 +55,13 @@ export const SmartGroupMemberLabelEditor = memo(
return (
(null);",
+ "reasonCategory": "usageTrusted",
+ "updated": "2026-02-03T20:48:35.470Z",
+ "reasonDetail": "A container passed to "
+ },
{
"rule": "React-useRef",
"path": "ts/components/conversation/media-gallery/MediaGallery.dom.tsx",
@@ -2386,12 +2401,5 @@
"line": " message.innerHTML = window.SignalContext.i18n('icu:optimizingApplication');",
"reasonCategory": "usageTrusted",
"updated": "2021-09-17T21:02:59.414Z"
- },
- {
- "rule": "React-useRef",
- "path": "ts/components/conversation/MessageContextMenu.dom.tsx",
- "line": " const shouldReturnFocusToTrigger = useRef(true);",
- "reasonCategory": "usageTrusted",
- "updated": "2025-12-19T16:03:53.849Z"
}
]
diff --git a/ts/util/truncateString.std.ts b/ts/util/truncateString.std.ts
new file mode 100644
index 0000000000..9db81209d9
--- /dev/null
+++ b/ts/util/truncateString.std.ts
@@ -0,0 +1,25 @@
+// Copyright 2026 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+import { count, truncateAndSize } from './grapheme.std.js';
+import { isBodyTooLong, trimBody } from './longAttachment.std.js';
+
+export function truncateString(
+ target: string,
+ {
+ byteLimit,
+ graphemeLimit,
+ }: { byteLimit?: number; graphemeLimit?: number } = {}
+): string {
+ let result = target;
+
+ if (byteLimit && isBodyTooLong(result, byteLimit)) {
+ result = trimBody(result, byteLimit);
+ }
+
+ if (graphemeLimit && count(result) > graphemeLimit) {
+ [result] = truncateAndSize(result, graphemeLimit);
+ }
+
+ return result;
+}