From b61c2029c461c989146aea3914731bc9064d2a5a Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Tue, 24 Feb 2026 03:48:13 +1000 Subject: [PATCH] GroupMemberLabelEditor: Deep links, warn on navigate away --- _locales/en/messages.json | 4 + stylesheets/components/AboutContactModal.scss | 14 +- .../AboutContactModal.dom.stories.tsx | 12 ++ .../conversation/AboutContactModal.dom.tsx | 138 ++++++++++++------ .../GroupMemberLabelEditor.dom.tsx | 40 ++++- ts/state/ducks/nav.std.ts | 1 + ts/state/selectors/nav.std.ts | 4 +- ts/state/smart/AboutContactModal.preload.tsx | 62 +++++++- ts/state/smart/ConversationPanel.preload.tsx | 23 ++- .../GroupMemberLabelInfoModal.preload.tsx | 43 ++++-- ts/types/Nav.std.ts | 6 +- 11 files changed, 262 insertions(+), 85 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d4dd363db7..ed61301aa4 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1482,6 +1482,10 @@ "messageformat": "Add a member label", "description": "Text for a row on the about contact modal, allowing user to add a label for themselves in the current group" }, + "icu:AboutContactModal__your-qr-code": { + "messageformat": "Your QR code", + "description": "Text for a row on the about contact modal, allowing user to add a label for themselves in the current group" + }, "icu:NotePreviewModal__Title": { "messageformat": "Note", "description": "Title of Note Preview modal" diff --git a/stylesheets/components/AboutContactModal.scss b/stylesheets/components/AboutContactModal.scss index 5d4e3dae3d..aa91e1e52e 100644 --- a/stylesheets/components/AboutContactModal.scss +++ b/stylesheets/components/AboutContactModal.scss @@ -119,6 +119,10 @@ &--label { @include about-modal-icon('../images/icons/v3/tag/tag.svg'); } + + &--qr-code { + @include about-modal-icon('../images/icons/v3/qr_code/qr_code.svg'); + } } &__label-container { @@ -128,14 +132,14 @@ max-width: 100%; white-space: nowrap; - overflow: hidden; + overflow-x: hidden; text-overflow: ellipsis; } &__label-container__string { min-width: 0px; white-space: nowrap; - overflow: hidden; + overflow-x: hidden; text-overflow: ellipsis; } @@ -146,7 +150,11 @@ min-width: 0; @include mixins.button-reset(); + @include mixins.button-focus-outline; + & { + border-radius: 3px; + padding: 1px; cursor: pointer; } @@ -178,7 +186,7 @@ .AboutContactModal__OneLineEllipsis { white-space: nowrap; - overflow: hidden; + overflow-x: hidden; text-overflow: ellipsis; } diff --git a/ts/components/conversation/AboutContactModal.dom.stories.tsx b/ts/components/conversation/AboutContactModal.dom.stories.tsx index d8d44d6452..b20a1a0787 100644 --- a/ts/components/conversation/AboutContactModal.dom.stories.tsx +++ b/ts/components/conversation/AboutContactModal.dom.stories.tsx @@ -74,6 +74,9 @@ export default { onOpenNotePreviewModal: action('onOpenNotePreviewModal'), pendingAvatarDownload: false, sharedGroupNames: [], + showProfileEditor: action('showProfileEditor'), + showQRCodeScreen: action('showQRCodeScreen'), + showEditMemberLabelScreen: action('showEditMemberLabelScreen'), startAvatarDownload: action('startAvatarDownload'), toggleProfileNameWarningModal: action('toggleProfileNameWarningModal'), toggleSafetyNumberModal: action('toggleSafetyNumberModal'), @@ -89,6 +92,15 @@ export function Me(args: PropsType): React.JSX.Element { return ; } +export function MeWithUsername(args: PropsType): React.JSX.Element { + return ( + + ); +} + export function MeWithLabel(args: PropsType): React.JSX.Element { return ( void; pendingAvatarDownload?: boolean; sharedGroupNames: ReadonlyArray; + showEditMemberLabelScreen: () => unknown; + showProfileEditor: () => unknown; + showQRCodeScreen: () => unknown; startAvatarDownload?: (id: string) => unknown; toggleSignalConnectionsModal: () => void; toggleSafetyNumberModal: (id: string) => void; @@ -62,6 +65,9 @@ export function AboutContactModal({ isSignalConnection, pendingAvatarDownload, sharedGroupNames, + showEditMemberLabelScreen, + showProfileEditor, + showQRCodeScreen, startAvatarDownload, toggleSignalConnectionsModal, toggleSafetyNumberModal, @@ -176,6 +182,42 @@ export function AboutContactModal({ ); } + const nameElement = + canHaveNicknameAndNote(contact) && + contact.titleNoNickname !== contact.title && + contact.titleNoNickname ? ( + + , + titleNoNickname: ( + , + }} + /> + } + delay={0} + > + + + ), + muted, + }} + /> + + ) : ( + + ); + return (
- - {canHaveNicknameAndNote(contact) && - contact.titleNoNickname !== contact.title && - contact.titleNoNickname ? ( - - , - titleNoNickname: ( - , - }} - /> - } - delay={0} - > - - - ), - muted, - }} - /> - + {isMe ? ( + ) : ( - + nameElement )}
{!isMe && !fromOrAddedByTrustedContact ? ( @@ -314,29 +332,53 @@ export function AboutContactModal({ {shouldShowLabel && (
-
- {labelEmojiElement} - - - -
+
)} {shouldShowAddLabel && (
- {i18n('icu:AboutContactModal__add-member-label')} + +
+ )} + {isMe && contact.username && ( +
+ +
)} - {contact.phoneNumber ? ( + {!isMe && contact.phoneNumber ? (
diff --git a/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx b/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx index 0da56454d4..66350c6714 100644 --- a/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx +++ b/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx @@ -12,7 +12,6 @@ import { isEmojiVariantValue, } from '../../fun/data/emojis.std.js'; import { FunEmojiPickerButton } from '../../fun/FunButton.dom.js'; - import { tw } from '../../../axo/tw.dom.js'; import { AxoButton } from '../../../axo/AxoButton.dom.js'; import { @@ -28,6 +27,12 @@ import { ConversationColors } from '../../../types/Colors.std.js'; import { WidthBreakpoint } from '../../_util.std.js'; import { AxoAlertDialog } from '../../../axo/AxoAlertDialog.dom.js'; import { SignalService as Proto } from '../../../protobuf/index.std.js'; +import { Avatar, AvatarSize } from '../../Avatar.dom.js'; +import { UserText } from '../../UserText.dom.js'; +import { GroupMemberLabel } from '../ContactName.dom.js'; +import { useConfirmDiscard } from '../../../hooks/useConfirmDiscard.dom.js'; +import { NavTab } from '../../../types/Nav.std.js'; +import { PanelType } from '../../../types/Panels.std.js'; import type { EmojiVariantKey } from '../../fun/data/emojis.std.js'; import type { @@ -36,9 +41,7 @@ import type { } from '../../../state/ducks/conversations.preload.js'; import type { LocalizerType, ThemeType } from '../../../types/Util.std.js'; import type { PreferredBadgeSelectorType } from '../../../state/selectors/badges.preload.js'; -import { Avatar, AvatarSize } from '../../Avatar.dom.js'; -import { UserText } from '../../UserText.dom.js'; -import { GroupMemberLabel } from '../ContactName.dom.js'; +import type { Location } from '../../../types/Nav.std.js'; export type PropsDataType = { existingLabelEmoji: string | undefined; @@ -63,6 +66,21 @@ export type PropsType = PropsDataType & { updateGroupMemberLabel: UpdateGroupMemberLabelType; }; +// We don't want to render any panel behind it as we animate it in, if we weren't already +// showing the ConversationDetails pane. +export function getLeafPanelOnly( + location: Location, + conversationId: string | undefined +): boolean { + return ( + !conversationId || + location.tab !== NavTab.Chats || + location.details.conversationId !== conversationId || + location.details.panels?.watermark === -1 || + location.details.panels?.stack[0]?.type !== PanelType.ConversationDetails + ); +} + function getEmojiVariantKey(value: string): EmojiVariantKey | undefined { if (isEmojiVariantValue(value)) { return getEmojiVariantKeyByValue(value); @@ -126,6 +144,19 @@ export function GroupMemberLabelEditor({ } }, [group, isShowingPermissionsError, setIsShowingPermissionsError]); + const tryClose = React.useRef<() => void | undefined>(); + const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard({ + i18n, + name: 'GroupMemberLabelEditor', + tryClose, + }); + + const onTryClose = React.useCallback(() => { + const discardChanges = noop; + confirmDiscardIf(isDirty, discardChanges); + }, [confirmDiscardIf, isDirty]); + tryClose.current = onTryClose; + return (
@@ -367,6 +398,7 @@ export function GroupMemberLabelEditor({ {i18n('icu:save')}
+ {confirmDiscardModal} { diff --git a/ts/state/ducks/nav.std.ts b/ts/state/ducks/nav.std.ts index 11bed629f8..90c93a455f 100644 --- a/ts/state/ducks/nav.std.ts +++ b/ts/state/ducks/nav.std.ts @@ -350,6 +350,7 @@ export function reducer( ...state.selectedLocation.details.panels, isAnimating: false, wasAnimated: true, + leafPanelOnly: false, }, }, }, diff --git a/ts/state/selectors/nav.std.ts b/ts/state/selectors/nav.std.ts index 397d6c75ff..717b1d5029 100644 --- a/ts/state/selectors/nav.std.ts +++ b/ts/state/selectors/nav.std.ts @@ -60,6 +60,7 @@ export const getActivePanel = createSelector( type PanelInformationType = { currPanel: PanelArgsType | undefined; + leafPanelOnly?: boolean; direction: 'push' | 'pop'; prevPanel: PanelArgsType | undefined; }; @@ -72,7 +73,7 @@ export const getPanelInformation = createSelector( return; } - const { direction, watermark } = panels; + const { direction, watermark, leafPanelOnly } = panels; if (!direction) { return; @@ -86,6 +87,7 @@ export const getPanelInformation = createSelector( currPanel, direction, prevPanel, + leafPanelOnly, }; } ); diff --git a/ts/state/smart/AboutContactModal.preload.tsx b/ts/state/smart/AboutContactModal.preload.tsx index 93dd8f38f0..600416bb55 100644 --- a/ts/state/smart/AboutContactModal.preload.tsx +++ b/ts/state/smart/AboutContactModal.preload.tsx @@ -21,9 +21,15 @@ import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPe 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'); +import { useNavActions } from '../ducks/nav.std.js'; +import { PanelType } from '../../types/Panels.std.js'; +import { + NavTab, + ProfileEditorPage, + SettingsPage, +} from '../../types/Nav.std.js'; +import { getSelectedLocation } from '../selectors/nav.std.js'; +import { getLeafPanelOnly } from '../../components/conversation/conversation-details/GroupMemberLabelEditor.dom.js'; function isFromOrAddedByTrustedContact( conversation: ConversationType @@ -57,10 +63,6 @@ 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 ?? ''); @@ -88,6 +90,10 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() { toggleNotePreviewModal, toggleProfileNameWarningModal, } = useGlobalModalActions(); + const { changeLocation } = useNavActions(); + + const selectedLocation = useSelector(getSelectedLocation); + const leafPanelOnly = getLeafPanelOnly(selectedLocation, conversationId); const handleOpenNotePreviewModal = useCallback(() => { strictAssert(contactId != null, 'contactId is required'); @@ -107,7 +113,7 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() { contactLabelString={contactLabelString} contactNameColor={contactNameColor} fromOrAddedByTrustedContact={isFromOrAddedByTrustedContact(contact)} - isEditMemberLabelEnabled={false} + isEditMemberLabelEnabled={isEditMemberLabelEnabled} isSignalConnection={isSignalConnection(contact)} onClose={toggleAboutContactModal} onOpenNotePreviewModal={handleOpenNotePreviewModal} @@ -115,6 +121,46 @@ export const SmartAboutContactModal = memo(function SmartAboutContactModal() { conversationId ? isPendingAvatarDownload(conversationId) : false } sharedGroupNames={sharedGroupNames} + showProfileEditor={() => { + changeLocation({ + tab: NavTab.Settings, + details: { + page: SettingsPage.Profile, + state: ProfileEditorPage.ProfileName, + }, + }); + toggleAboutContactModal(undefined); + }} + showQRCodeScreen={() => { + changeLocation({ + tab: NavTab.Settings, + details: { + page: SettingsPage.Profile, + state: ProfileEditorPage.UsernameLink, + }, + }); + toggleAboutContactModal(undefined); + }} + showEditMemberLabelScreen={() => { + changeLocation({ + tab: NavTab.Chats, + details: { + conversationId, + panels: { + direction: 'push' as const, + isAnimating: false, + leafPanelOnly, + stack: [ + { type: PanelType.ConversationDetails }, + { type: PanelType.GroupMemberLabelEditor }, + ], + wasAnimated: false, + watermark: 1, + }, + }, + }); + toggleAboutContactModal(undefined); + }} startAvatarDownload={ conversationId ? () => startAvatarDownload(conversationId) : undefined } diff --git a/ts/state/smart/ConversationPanel.preload.tsx b/ts/state/smart/ConversationPanel.preload.tsx index f22818bd0c..3ac93e9954 100644 --- a/ts/state/smart/ConversationPanel.preload.tsx +++ b/ts/state/smart/ConversationPanel.preload.tsx @@ -209,7 +209,12 @@ export const ConversationPanel = memo(function ConversationPanel({ return null; } - const { currPanel: activePanel, direction, prevPanel } = panelInformation; + const { + currPanel: activePanel, + direction, + leafPanelOnly, + prevPanel, + } = panelInformation; if (!direction) { return null; @@ -248,13 +253,15 @@ export const ConversationPanel = memo(function ConversationPanel({ if (direction === 'push' && activePanel) { return ( <> - {lastPanelDoneAnimating !== prevPanel && prevPanel && ( - - )} + {!leafPanelOnly && + lastPanelDoneAnimating !== prevPanel && + prevPanel && ( + + )}
user.ourAci && membership.aci === user.ourAci ); const hasLabel = Boolean(contactMembership?.labelString); const canAddLabel = getCanAddLabel(conversation, contactMembership); - const { toggleGroupMemberLabelInfoModal } = useGlobalModalActions(); + const { toggleGroupMemberLabelInfoModal, hideContactModal } = + useGlobalModalActions(); return ( toggleGroupMemberLabelInfoModal(undefined)} showEditMemberLabelScreen={() => { - // TODO: DESKTOP-9711 - throw new Error('Not yet implemented'); + changeLocation({ + tab: NavTab.Chats, + details: { + conversationId, + panels: { + direction: 'push' as const, + isAnimating: false, + leafPanelOnly, + stack: [ + { type: PanelType.ConversationDetails }, + { type: PanelType.GroupMemberLabelEditor }, + ], + wasAnimated: false, + watermark: 1, + }, + }, + }); + toggleGroupMemberLabelInfoModal(undefined); + hideContactModal(); }} /> ); diff --git a/ts/types/Nav.std.ts b/ts/types/Nav.std.ts index 29c1d22b40..ff91c2b894 100644 --- a/ts/types/Nav.std.ts +++ b/ts/types/Nav.std.ts @@ -23,10 +23,12 @@ export type ChatDetails = ReadonlyDeep<{ }>; export type PanelInfo = { - isAnimating: boolean; - wasAnimated: boolean; direction: 'push' | 'pop' | undefined; + isAnimating: boolean; + // When navigating deep into a panel stack, we only want to render the leaf panel + leafPanelOnly?: boolean; stack: ReadonlyArray; + wasAnimated: boolean; watermark: number; };