GroupMemberLabelEditor: Deep links, warn on navigate away

This commit is contained in:
Scott Nonnenberg
2026-02-24 03:48:13 +10:00
committed by GitHub
parent 772c3c22ad
commit b61c2029c4
11 changed files with 262 additions and 85 deletions

View File

@@ -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"

View File

@@ -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;
}

View File

@@ -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 <AboutContactModal {...args} contact={me} />;
}
export function MeWithUsername(args: PropsType): React.JSX.Element {
return (
<AboutContactModal
{...args}
contact={{ ...me, username: 'myusername.04' }}
/>
);
}
export function MeWithLabel(args: PropsType): React.JSX.Element {
return (
<AboutContactModal

View File

@@ -44,6 +44,9 @@ export type PropsType = Readonly<{
onOpenNotePreviewModal: () => void;
pendingAvatarDownload?: boolean;
sharedGroupNames: ReadonlyArray<string>;
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 ? (
<span>
<I18n
i18n={i18n}
id="icu:AboutContactModal__TitleAndTitleWithoutNickname"
components={{
nickname: <UserText text={contact.title} />,
titleNoNickname: (
<Tooltip
className="AboutContactModal__TitleWithoutNickname__Tooltip"
direction={TooltipPlacement.Top}
content={
<I18n
i18n={i18n}
id="icu:AboutContactModal__TitleWithoutNickname__Tooltip"
components={{
title: <UserText text={contact.titleNoNickname} />,
}}
/>
}
delay={0}
>
<UserText text={contact.titleNoNickname} />
</Tooltip>
),
muted,
}}
/>
</span>
) : (
<UserText text={contact.title} />
);
return (
<Modal
key="main"
@@ -211,40 +253,16 @@ export function AboutContactModal({
</div>
<div className="AboutContactModal__row">
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--profile" />
{canHaveNicknameAndNote(contact) &&
contact.titleNoNickname !== contact.title &&
contact.titleNoNickname ? (
<span>
<I18n
i18n={i18n}
id="icu:AboutContactModal__TitleAndTitleWithoutNickname"
components={{
nickname: <UserText text={contact.title} />,
titleNoNickname: (
<Tooltip
className="AboutContactModal__TitleWithoutNickname__Tooltip"
direction={TooltipPlacement.Top}
content={
<I18n
i18n={i18n}
id="icu:AboutContactModal__TitleWithoutNickname__Tooltip"
components={{
title: <UserText text={contact.titleNoNickname} />,
}}
/>
}
delay={0}
>
<UserText text={contact.titleNoNickname} />
</Tooltip>
),
muted,
}}
/>
</span>
{isMe ? (
<button
className="AboutContactModal__button"
type="button"
onClick={showProfileEditor}
>
{nameElement}
</button>
) : (
<UserText text={contact.title} />
nameElement
)}
</div>
{!isMe && !fromOrAddedByTrustedContact ? (
@@ -314,29 +332,53 @@ export function AboutContactModal({
{shouldShowLabel && (
<div className="AboutContactModal__row">
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--label" />
<div className="AboutContactModal__label-container">
{labelEmojiElement}
<span className="AboutContactModal__label-container__string">
<UserText
fontSizeOverride={14}
style={{
verticalAlign: 'top',
marginTop: '3px',
}}
text={contactLabelString}
/>
</span>
</div>
<button
className="AboutContactModal__button"
type="button"
onClick={showEditMemberLabelScreen}
>
<div className="AboutContactModal__label-container">
{labelEmojiElement}
<span className="AboutContactModal__label-container__string">
<UserText
fontSizeOverride={14}
style={{
verticalAlign: 'top',
marginTop: '3px',
}}
text={contactLabelString}
/>
</span>
</div>
</button>
</div>
)}
{shouldShowAddLabel && (
<div className="AboutContactModal__row">
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--label" />
{i18n('icu:AboutContactModal__add-member-label')}
<button
className="AboutContactModal__button"
type="button"
onClick={showEditMemberLabelScreen}
>
{i18n('icu:AboutContactModal__add-member-label')}
</button>
</div>
)}
{isMe && contact.username && (
<div className="AboutContactModal__row">
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--qr-code" />
<button
className="AboutContactModal__button"
type="button"
onClick={showQRCodeScreen}
>
{i18n('icu:AboutContactModal__your-qr-code')}
</button>
</div>
)}
{contact.phoneNumber ? (
{!isMe && contact.phoneNumber ? (
<div className="AboutContactModal__row">
<i className="AboutContactModal__row__icon AboutContactModal__row__icon--phone" />
<UserText text={contact.phoneNumber} />

View File

@@ -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 (
<div className={tw('flex size-full flex-col')}>
<div className={tw('grow flex-col overflow-y-scroll')}>
@@ -367,6 +398,7 @@ export function GroupMemberLabelEditor({
{i18n('icu:save')}
</AxoButton.Root>
</div>
{confirmDiscardModal}
<AxoAlertDialog.Root
open={isShowingGeneralError}
onOpenChange={value => {

View File

@@ -350,6 +350,7 @@ export function reducer(
...state.selectedLocation.details.panels,
isAnimating: false,
wasAnimated: true,
leafPanelOnly: false,
},
},
},

View File

@@ -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,
};
}
);

View File

@@ -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
}

View File

@@ -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 && (
<PanelContainer
conversationId={conversationId}
panel={prevPanel}
key={getPanelKey(prevPanel)}
/>
)}
{!leafPanelOnly &&
lastPanelDoneAnimating !== prevPanel &&
prevPanel && (
<PanelContainer
conversationId={conversationId}
panel={prevPanel}
key={getPanelKey(prevPanel)}
/>
)}
<div
key="overlay"
className="ConversationPanel__overlay"

View File

@@ -11,9 +11,11 @@ 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');
import { useNavActions } from '../ducks/nav.std.js';
import { NavTab } from '../../types/Nav.std.js';
import { PanelType } from '../../types/Panels.std.js';
import { getSelectedLocation } from '../selectors/nav.std.js';
import { getLeafPanelOnly } from '../../components/conversation/conversation-details/GroupMemberLabelEditor.dom.js';
export const SmartGroupMemberLabelInfoModal = memo(
function SmartGroupMemberLabelInfoModal() {
@@ -25,37 +27,56 @@ export const SmartGroupMemberLabelInfoModal = memo(
useSelector(getGroupMemberLabelInfoModalState) ?? {};
const getConversation = useSelector(getConversationSelector);
const { changeLocation } = useNavActions();
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 selectedLocation = useSelector(getSelectedLocation);
const leafPanelOnly = getLeafPanelOnly(selectedLocation, 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();
const { toggleGroupMemberLabelInfoModal, hideContactModal } =
useGlobalModalActions();
return (
<GroupMemberLabelInfoModal
i18n={i18n}
canAddLabel={canAddLabel}
hasLabel={hasLabel}
isEditMemberLabelEnabled={false}
isEditMemberLabelEnabled={isEditMemberLabelEnabled}
onClose={() => 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();
}}
/>
);

View File

@@ -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<PanelArgsType>;
wasAnimated: boolean;
watermark: number;
};