Add alert dialog to confirm replacing oldest pinned message

Co-authored-by: Jamie <113370520+jamiebuilds-signal@users.noreply.github.com>
This commit is contained in:
automated-signal
2025-12-16 11:12:12 -06:00
committed by GitHub
parent 1b5b2a1d99
commit 2d979adebe
9 changed files with 149 additions and 59 deletions

View File

@@ -1658,6 +1658,22 @@
"messageformat": "Pin",
"description": "Message > Context Menu > Pin Message > Dialog > Pin Button"
},
"icu:PinMessageDialog--HasMaxPinnedMessages__Title": {
"messageformat": "Replace oldest pin?",
"description": "Message > Context Menu > Pin Message (when at max pinned messages) > Dialog > Title"
},
"icu:PinMessageDialog--HasMaxPinnedMessages__Description": {
"messageformat": "Pinning this message will replace the oldest one.",
"description": "Message > Context Menu > Pin Message (when at max pinned messages) > Dialog > Description"
},
"icu:PinMessageDialog--HasMaxPinnedMessages__Cancel": {
"messageformat": "Cancel",
"description": "Message > Context Menu > Pin Message (when at max pinned messages) > Dialog > Cancel Button"
},
"icu:PinMessageDialog--HasMaxPinnedMessages__Continue": {
"messageformat": "Continue",
"description": "Message > Context Menu > Pin Message (when at max pinned messages) > Dialog > Continue Button"
},
"icu:PinnedMessagesBar__AccessibilityLabel": {
"messageformat": "{pinsCount, plural, one {Pinned message} other {Pinned messages}}",
"description": "Conversation > With pinned message(s) > Pinned messages bar > Accessibility label"

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { AlertDialog } from 'radix-ui';
import type { FC, ReactNode } from 'react';
import type { FC, MouseEvent, ReactNode } from 'react';
import React, { memo } from 'react';
import { AxoButton } from './AxoButton.dom.js';
import { tw } from './tw.dom.js';
@@ -221,7 +221,7 @@ export namespace AxoAlertDialog {
variant: ActionVariant;
symbol?: AxoSymbol.InlineGlyphName;
arrow?: boolean;
onClick: () => void;
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
children: ReactNode;
}>;

View File

@@ -98,6 +98,7 @@ const defaultMessageProps: TimelineMessagesProps = {
'default--doubleCheckMissingQuoteReference'
),
getPreferredBadge: () => undefined,
hasMaxPinnedMessages: false,
i18n,
platform: 'darwin',
id: 'messageId',

View File

@@ -66,6 +66,7 @@ function mockMessageTimelineItem(
direction: 'incoming',
status: 'sent',
text: 'Hello there from the new world!',
hasMaxPinnedMessages: false,
isBlocked: false,
isMessageRequestAccepted: true,
isPinned: false,

View File

@@ -265,6 +265,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
expirationTimestamp: overrideProps.expirationTimestamp ?? 0,
getPreferredBadge: overrideProps.getPreferredBadge || (() => undefined),
giftBadge: overrideProps.giftBadge,
hasMaxPinnedMessages: false,
i18n,
platform: 'darwin',
id: overrideProps.id ?? 'random-message-id',

View File

@@ -55,6 +55,7 @@ export type PropsData = {
canReact: boolean;
canReply: boolean;
canPinMessages: boolean;
hasMaxPinnedMessages: boolean;
selectedReaction?: string;
isTargeted?: boolean;
} & Omit<MessagePropsData, 'renderingContext' | 'menu'>;
@@ -118,6 +119,7 @@ export function TimelineMessage(props: Props): JSX.Element {
containerWidthBreakpoint,
conversationId,
direction,
hasMaxPinnedMessages,
i18n,
id,
interactivity,
@@ -493,6 +495,7 @@ export function TimelineMessage(props: Props): JSX.Element {
open={pinMessageDialogOpen}
onOpenChange={setPinMessageDialogOpen}
onPinnedMessageAdd={handlePinnedMessageAdd}
hasMaxPinnedMessages={hasMaxPinnedMessages}
/>
</>
);

View File

@@ -20,6 +20,7 @@ export function Default(): JSX.Element {
onOpenChange={setOpen}
messageId="42"
onPinnedMessageAdd={action('onPinnedMessageAdd')}
hasMaxPinnedMessages={false}
/>
);
}

View File

@@ -1,11 +1,13 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { MouseEvent } from 'react';
import React, { memo, useCallback, useState } from 'react';
import { AxoDialog } from '../../../axo/AxoDialog.dom.js';
import type { LocalizerType } from '../../../types/I18N.std.js';
import { AxoRadioGroup } from '../../../axo/AxoRadioGroup.dom.js';
import { DurationInSeconds } from '../../../util/durations/duration-in-seconds.std.js';
import { strictAssert } from '../../../util/assert.std.js';
import { AxoAlertDialog } from '../../../axo/AxoAlertDialog.dom.js';
enum DurationOption {
TIME_24_HOURS = 'TIME_24_HOURS',
@@ -30,6 +32,7 @@ export type PinMessageDialogProps = Readonly<{
open: boolean;
onOpenChange: (open: boolean) => void;
messageId: string;
hasMaxPinnedMessages: boolean;
onPinnedMessageAdd: (
messageId: string,
duration: DurationInSeconds | null
@@ -41,6 +44,23 @@ export const PinMessageDialog = memo(function PinMessageDialog(
) {
const { i18n, messageId, onPinnedMessageAdd, onOpenChange } = props;
const [duration, setDuration] = useState(DurationOption.TIME_7_DAYS);
const [confirmedReplaceOldestPin, setConfirmedReplaceOldestPin] =
useState(false);
const handleOpenChange = useCallback(
(open: boolean) => {
onOpenChange(open);
// reset state
setConfirmedReplaceOldestPin(false);
setDuration(DurationOption.TIME_7_DAYS);
},
[onOpenChange]
);
const handleConfirmReplaceOldestPin = useCallback((event: MouseEvent) => {
event.preventDefault();
setConfirmedReplaceOldestPin(true);
}, []);
const handleValueChange = useCallback((value: string) => {
strictAssert(isValidDurationOption(value), `Invalid option: ${value}`);
@@ -48,16 +68,50 @@ export const PinMessageDialog = memo(function PinMessageDialog(
}, []);
const handleCancel = useCallback(() => {
onOpenChange(false);
}, [onOpenChange]);
handleOpenChange(false);
}, [handleOpenChange]);
const handlePinnedMessageAdd = useCallback(() => {
const durationValue = DURATION_OPTIONS[duration];
onPinnedMessageAdd(messageId, durationValue);
}, [duration, onPinnedMessageAdd, messageId]);
const showConfirmReplaceOldestPin =
props.hasMaxPinnedMessages && !confirmedReplaceOldestPin;
return (
<AxoDialog.Root open={props.open} onOpenChange={onOpenChange}>
<>
<AxoAlertDialog.Root
open={props.open && showConfirmReplaceOldestPin}
onOpenChange={handleOpenChange}
>
<AxoAlertDialog.Content escape="cancel-is-noop">
<AxoAlertDialog.Body>
<AxoAlertDialog.Title>
{i18n('icu:PinMessageDialog--HasMaxPinnedMessages__Title')}
</AxoAlertDialog.Title>
<AxoAlertDialog.Description>
{i18n('icu:PinMessageDialog--HasMaxPinnedMessages__Description')}
</AxoAlertDialog.Description>
</AxoAlertDialog.Body>
<AxoAlertDialog.Footer>
<AxoAlertDialog.Cancel>
{i18n('icu:PinMessageDialog--HasMaxPinnedMessages__Cancel')}
</AxoAlertDialog.Cancel>
<AxoAlertDialog.Action
variant="primary"
onClick={handleConfirmReplaceOldestPin}
>
{i18n('icu:PinMessageDialog--HasMaxPinnedMessages__Continue')}
</AxoAlertDialog.Action>
</AxoAlertDialog.Footer>
</AxoAlertDialog.Content>
</AxoAlertDialog.Root>
<AxoDialog.Root
open={props.open && !showConfirmReplaceOldestPin}
onOpenChange={handleOpenChange}
>
<AxoDialog.Content
size="sm"
escape="cancel-is-noop"
@@ -115,5 +169,6 @@ export const PinMessageDialog = memo(function PinMessageDialog(
</AxoDialog.Footer>
</AxoDialog.Content>
</AxoDialog.Root>
</>
);
});

View File

@@ -170,6 +170,7 @@ import type { PinnedMessageNotificationData } from '../../components/conversatio
import { canEditGroupInfo } from '../../util/canEditGroupInfo.preload.js';
import { getPinnedMessagesMessageIds } from './pinnedMessages.dom.js';
import { isPinnedMessagesSendEnabled } from '../../util/isPinnedMessagesEnabled.dom.js';
import { getPinnedMessagesLimit } from '../../util/pinnedMessages.dom.js';
const { groupBy, isEmpty, isNumber, isObject, map } = lodash;
@@ -966,6 +967,9 @@ export const getPropsForMessage = (
expirationStartTimestamp,
}),
giftBadge: message.giftBadge,
hasMaxPinnedMessages: getHasMaxPinnedMessages(
options.pinnedMessagesMessageIds ?? []
),
poll: getPollForMessage(message, {
conversationSelector: options.conversationSelector,
ourConversationId,
@@ -2410,6 +2414,14 @@ export function canPinMessages(conversation: ConversationType): boolean {
return conversation.type === 'direct' || canEditGroupInfo(conversation);
}
function getHasMaxPinnedMessages(
pinnedMessagesMessageIds: ReadonlyArray<string>
) {
const pinnedMessagesLimit = getPinnedMessagesLimit();
const pinnedMessagesCount = pinnedMessagesMessageIds.length;
return pinnedMessagesCount >= pinnedMessagesLimit;
}
export function getLastChallengeError(
message: Pick<MessageWithUIFieldsType, 'errors'>
): ShallowChallengeError | undefined {