mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-02-15 07:28:59 +00:00
Add warning when pinning disappearing message
This commit is contained in:
@@ -1650,6 +1650,18 @@
|
||||
"messageformat": "Continue",
|
||||
"description": "Message > Context Menu > Pin Message (when at max pinned messages) > Dialog > Continue Button"
|
||||
},
|
||||
"icu:PinMessageDisappearingMessagesWarningDialog__Title": {
|
||||
"messageformat": "Pinning disappearing messages",
|
||||
"description": "Message > Context Menu > Pin Message (when pinning disappearing message) > Dialog > Title"
|
||||
},
|
||||
"icu:PinMessageDisappearingMessagesWarningDialog__Description": {
|
||||
"messageformat": "Disappearing messages will be unpinned when their timer expires and the message is removed from the chat.",
|
||||
"description": "Message > Context Menu > Pin Message (when pinning disappearing message) > Dialog > Description"
|
||||
},
|
||||
"icu:PinMessageDisappearingMessagesWarningDialog__Okay": {
|
||||
"messageformat": "Okay",
|
||||
"description": "Message > Context Menu > Pin Message (when pinning disappearing message) > Dialog > Okay Button"
|
||||
},
|
||||
"icu:PinnedMessagesBar__AccessibilityLabel": {
|
||||
"messageformat": "{pinsCount, plural, one {Pinned message} other {Pinned messages}}",
|
||||
"description": "Conversation > With pinned message(s) > Pinned messages bar > Accessibility label"
|
||||
|
||||
@@ -35,6 +35,7 @@ import type { SmartDraftGifMessageSendModalProps } from '../state/smart/DraftGif
|
||||
import { CriticalIdlePrimaryDeviceModal } from './CriticalIdlePrimaryDeviceModal.dom.js';
|
||||
import { LowDiskSpaceBackupImportModal } from './LowDiskSpaceBackupImportModal.dom.js';
|
||||
import { isUsernameValid } from '../util/Username.dom.js';
|
||||
import type { PinMessageDialogData } from '../state/smart/PinMessageDialog.preload.js';
|
||||
|
||||
// NOTE: All types should be required for this component so that the smart
|
||||
// component gives you type errors when adding/removing props.
|
||||
@@ -111,6 +112,9 @@ export type PropsType = {
|
||||
// MessageRequestActionsConfirmation
|
||||
messageRequestActionsConfirmationProps: MessageRequestActionsConfirmationPropsType | null;
|
||||
renderMessageRequestActionsConfirmation: () => React.JSX.Element;
|
||||
// PinMessageDialog
|
||||
pinMessageDialogData: PinMessageDialogData | null;
|
||||
renderPinMessageDialog: () => React.JSX.Element;
|
||||
// NotePreviewModal
|
||||
notePreviewModalProps: { conversationId: string } | null;
|
||||
renderNotePreviewModal: () => React.JSX.Element;
|
||||
@@ -224,6 +228,9 @@ export function GlobalModalContainer({
|
||||
// NotePreviewModal
|
||||
notePreviewModalProps,
|
||||
renderNotePreviewModal,
|
||||
// PinMessageDialog
|
||||
pinMessageDialogData,
|
||||
renderPinMessageDialog,
|
||||
// SafetyNumberModal
|
||||
safetyNumberModalContactId,
|
||||
renderSafetyNumber,
|
||||
@@ -370,6 +377,10 @@ export function GlobalModalContainer({
|
||||
return renderNotePreviewModal();
|
||||
}
|
||||
|
||||
if (pinMessageDialogData) {
|
||||
return renderPinMessageDialog();
|
||||
}
|
||||
|
||||
if (isProfileNameWarningModalVisible) {
|
||||
return renderProfileNameWarningModal();
|
||||
}
|
||||
|
||||
@@ -98,7 +98,6 @@ const defaultMessageProps: TimelineMessagesProps = {
|
||||
'default--doubleCheckMissingQuoteReference'
|
||||
),
|
||||
getPreferredBadge: () => undefined,
|
||||
hasMaxPinnedMessages: false,
|
||||
i18n,
|
||||
platform: 'darwin',
|
||||
id: 'messageId',
|
||||
@@ -140,7 +139,7 @@ const defaultMessageProps: TimelineMessagesProps = {
|
||||
shouldCollapseBelow: false,
|
||||
shouldHideMetadata: false,
|
||||
showSpoiler: action('showSpoiler'),
|
||||
onPinnedMessageAdd: action('onPinnedMessageAdd'),
|
||||
showPinMessageDialog: action('showPinMessageDialog'),
|
||||
onPinnedMessageRemove: action('onPinnedMessageRemove'),
|
||||
pushPanelForConversation: action('default--pushPanelForConversation'),
|
||||
showContactModal: action('default--showContactModal'),
|
||||
|
||||
@@ -58,7 +58,6 @@ function mockMessageTimelineItem(
|
||||
direction: 'incoming',
|
||||
status: 'sent',
|
||||
text: 'Hello there from the new world!',
|
||||
hasMaxPinnedMessages: false,
|
||||
isBlocked: false,
|
||||
isMessageRequestAccepted: true,
|
||||
isPinned: false,
|
||||
@@ -307,7 +306,7 @@ const actions = () => ({
|
||||
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||
|
||||
openGiftBadge: action('openGiftBadge'),
|
||||
onPinnedMessageAdd: action('onPinnedMessageAdd'),
|
||||
showPinMessageDialog: action('showPinMessageDialog'),
|
||||
onPinnedMessageRemove: action('onPinnedMessageRemove'),
|
||||
scrollToPinnedMessage: action('scrollToPinnedMessage'),
|
||||
scrollToPollMessage: action('scrollToPollMessage'),
|
||||
|
||||
@@ -70,7 +70,7 @@ const getDefaultProps = () => ({
|
||||
openGiftBadge: action('openGiftBadge'),
|
||||
saveAttachment: action('saveAttachment'),
|
||||
saveAttachments: action('saveAttachments'),
|
||||
onPinnedMessageAdd: action('onPinnedMessageAdd'),
|
||||
showPinMessageDialog: action('showPinMessageDialog'),
|
||||
onPinnedMessageRemove: action('onPinnedMessageRemove'),
|
||||
onOpenEditNicknameAndNoteModal: action('onOpenEditNicknameAndNoteModal'),
|
||||
onOutgoingAudioCallInConversation: action(
|
||||
|
||||
@@ -265,7 +265,6 @@ 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',
|
||||
@@ -300,7 +299,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
messageExpanded: action('messageExpanded'),
|
||||
showConversation: action('showConversation'),
|
||||
openGiftBadge: action('openGiftBadge'),
|
||||
onPinnedMessageAdd: action('onPinnedMessageAdd'),
|
||||
showPinMessageDialog: action('showPinMessageDialog'),
|
||||
onPinnedMessageRemove: action('onPinnedMessageRemove'),
|
||||
previews: overrideProps.previews || [],
|
||||
quote: overrideProps.quote || undefined,
|
||||
|
||||
@@ -36,8 +36,6 @@ import { useGroupedAndOrderedReactions } from '../../util/groupAndOrderReactions
|
||||
import { isNotNil } from '../../util/isNotNil.std.js';
|
||||
import type { AxoMenuBuilder } from '../../axo/AxoMenuBuilder.dom.js';
|
||||
import { AxoContextMenu } from '../../axo/AxoContextMenu.dom.js';
|
||||
import { PinMessageDialog } from './pinned-messages/PinMessageDialog.dom.js';
|
||||
import type { DurationInSeconds } from '../../util/durations/duration-in-seconds.std.js';
|
||||
import { useDocumentKeyDown } from '../../hooks/useDocumentKeyDown.dom.js';
|
||||
|
||||
const { useAxoContextMenuOutsideKeyboardTrigger } = AxoContextMenu;
|
||||
@@ -55,16 +53,11 @@ export type PropsData = {
|
||||
canReact: boolean;
|
||||
canReply: boolean;
|
||||
canPinMessage: boolean;
|
||||
hasMaxPinnedMessages: boolean;
|
||||
selectedReaction?: string;
|
||||
isTargeted?: boolean;
|
||||
} & Omit<MessagePropsData, 'renderingContext' | 'menu'>;
|
||||
|
||||
export type PropsActions = {
|
||||
onPinnedMessageAdd: (
|
||||
messageId: string,
|
||||
duration: DurationInSeconds | null
|
||||
) => void;
|
||||
onPinnedMessageRemove: (messageId: string) => void;
|
||||
pushPanelForConversation: PushPanelForConversationActionType;
|
||||
toggleDeleteMessagesModal: (props: DeleteMessagesPropsType) => void;
|
||||
@@ -89,6 +82,10 @@ export type PropsActions = {
|
||||
shift: boolean,
|
||||
selected: boolean
|
||||
) => void;
|
||||
showPinMessageDialog: (
|
||||
messageId: string,
|
||||
isPinningDisappearingMessage: boolean
|
||||
) => void;
|
||||
} & Omit<MessagePropsActions, 'onToggleSelect' | 'onReplyToMessage'>;
|
||||
|
||||
export type Props = PropsData &
|
||||
@@ -119,7 +116,6 @@ export function TimelineMessage(props: Props): React.JSX.Element {
|
||||
containerWidthBreakpoint,
|
||||
conversationId,
|
||||
direction,
|
||||
hasMaxPinnedMessages,
|
||||
i18n,
|
||||
id,
|
||||
interactivity,
|
||||
@@ -128,7 +124,7 @@ export function TimelineMessage(props: Props): React.JSX.Element {
|
||||
kickOffAttachmentDownload,
|
||||
copyMessageText,
|
||||
endPoll,
|
||||
onPinnedMessageAdd,
|
||||
expirationLength,
|
||||
onPinnedMessageRemove,
|
||||
pushPanelForConversation,
|
||||
reactToMessage,
|
||||
@@ -138,6 +134,7 @@ export function TimelineMessage(props: Props): React.JSX.Element {
|
||||
saveAttachment,
|
||||
saveAttachments,
|
||||
showAttachmentDownloadStillInProgressToast,
|
||||
showPinMessageDialog,
|
||||
selectedReaction,
|
||||
setQuoteByMessageId,
|
||||
setMessageToEdit,
|
||||
@@ -151,7 +148,6 @@ export function TimelineMessage(props: Props): React.JSX.Element {
|
||||
const [reactionPickerRoot, setReactionPickerRoot] = useState<
|
||||
HTMLDivElement | undefined
|
||||
>(undefined);
|
||||
const [pinMessageDialogOpen, setPinMessageDialogOpen] = useState(false);
|
||||
|
||||
const isWindowWidthNotNarrow =
|
||||
containerWidthBreakpoint !== WidthBreakpoint.Narrow;
|
||||
@@ -286,17 +282,11 @@ export function TimelineMessage(props: Props): React.JSX.Element {
|
||||
}
|
||||
}, [canReact, toggleReactionPicker]);
|
||||
|
||||
const handleOpenPinMessageDialog = useCallback(() => {
|
||||
setPinMessageDialogOpen(true);
|
||||
}, []);
|
||||
const isDisappearingMessage = expirationLength != null;
|
||||
|
||||
const handlePinnedMessageAdd = useCallback(
|
||||
(messageId: string, duration: DurationInSeconds | null) => {
|
||||
onPinnedMessageAdd(messageId, duration);
|
||||
setPinMessageDialogOpen(false);
|
||||
},
|
||||
[onPinnedMessageAdd]
|
||||
);
|
||||
const handleOpenPinMessageDialog = useCallback(() => {
|
||||
showPinMessageDialog(id, isDisappearingMessage);
|
||||
}, [showPinMessageDialog, id, isDisappearingMessage]);
|
||||
|
||||
const handleUnpinMessage = useCallback(() => {
|
||||
onPinnedMessageRemove(id);
|
||||
@@ -478,27 +468,17 @@ export function TimelineMessage(props: Props): React.JSX.Element {
|
||||
const handleWrapperKeyDown = useAxoContextMenuOutsideKeyboardTrigger();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Message
|
||||
{...props}
|
||||
renderingContext="conversation/TimelineItem"
|
||||
renderMenu={renderMenu}
|
||||
renderMessageContextMenu={renderMessageContextMenu}
|
||||
onToggleSelect={(selected, shift) => {
|
||||
toggleSelectMessage(conversationId, id, shift, selected);
|
||||
}}
|
||||
onReplyToMessage={handleReplyToMessage}
|
||||
onWrapperKeyDown={handleWrapperKeyDown}
|
||||
/>
|
||||
<PinMessageDialog
|
||||
i18n={i18n}
|
||||
messageId={id}
|
||||
open={pinMessageDialogOpen}
|
||||
onOpenChange={setPinMessageDialogOpen}
|
||||
onPinnedMessageAdd={handlePinnedMessageAdd}
|
||||
hasMaxPinnedMessages={hasMaxPinnedMessages}
|
||||
/>
|
||||
</>
|
||||
<Message
|
||||
{...props}
|
||||
renderingContext="conversation/TimelineItem"
|
||||
renderMenu={renderMenu}
|
||||
renderMessageContextMenu={renderMessageContextMenu}
|
||||
onToggleSelect={(selected, shift) => {
|
||||
toggleSelectMessage(conversationId, id, shift, selected);
|
||||
}}
|
||||
onReplyToMessage={handleReplyToMessage}
|
||||
onWrapperKeyDown={handleWrapperKeyDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,12 @@ export function Default(): React.JSX.Element {
|
||||
onOpenChange={setOpen}
|
||||
messageId="42"
|
||||
onPinnedMessageAdd={action('onPinnedMessageAdd')}
|
||||
hasMaxPinnedMessages={false}
|
||||
hasMaxPinnedMessages
|
||||
isPinningDisappearingMessage
|
||||
seenPinMessageDisappearingMessagesWarningCount={0}
|
||||
onSeenPinMessageDisappearingMessagesWarning={action(
|
||||
'onSeenPinMessageDisappearingMessagesWarning'
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 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 React, { memo, useCallback, useMemo, 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';
|
||||
@@ -26,6 +26,13 @@ const DURATION_OPTIONS: Record<DurationOption, DurationInSeconds | null> = {
|
||||
[DurationOption.DEBUG_10_SECONDS]: DurationInSeconds.fromSeconds(10),
|
||||
};
|
||||
|
||||
enum Step {
|
||||
CLOSED,
|
||||
CONFIRM_REPLACE_OLDEST_PIN,
|
||||
SELECT_PIN_DURATION,
|
||||
DISAPPEARING_MESSAGES_WARNING,
|
||||
}
|
||||
|
||||
function isValidDurationOption(value: string): value is DurationOption {
|
||||
return Object.hasOwn(DURATION_OPTIONS, value);
|
||||
}
|
||||
@@ -36,6 +43,9 @@ export type PinMessageDialogProps = Readonly<{
|
||||
onOpenChange: (open: boolean) => void;
|
||||
messageId: string;
|
||||
hasMaxPinnedMessages: boolean;
|
||||
isPinningDisappearingMessage: boolean;
|
||||
seenPinMessageDisappearingMessagesWarningCount: number;
|
||||
onSeenPinMessageDisappearingMessagesWarning: () => void;
|
||||
onPinnedMessageAdd: (
|
||||
messageId: string,
|
||||
duration: DurationInSeconds | null
|
||||
@@ -45,8 +55,31 @@ export type PinMessageDialogProps = Readonly<{
|
||||
export const PinMessageDialog = memo(function PinMessageDialog(
|
||||
props: PinMessageDialogProps
|
||||
) {
|
||||
const { i18n, messageId, onPinnedMessageAdd, onOpenChange } = props;
|
||||
const [duration, setDuration] = useState(DurationOption.TIME_7_DAYS);
|
||||
const {
|
||||
i18n,
|
||||
onOpenChange,
|
||||
messageId,
|
||||
hasMaxPinnedMessages,
|
||||
isPinningDisappearingMessage,
|
||||
onPinnedMessageAdd,
|
||||
seenPinMessageDisappearingMessagesWarningCount,
|
||||
onSeenPinMessageDisappearingMessagesWarning,
|
||||
} = props;
|
||||
|
||||
const needsConfirmReplaceOldestPin = useMemo(() => {
|
||||
return hasMaxPinnedMessages;
|
||||
}, [hasMaxPinnedMessages]);
|
||||
const needsConfirmDisappearingMessages = useMemo(() => {
|
||||
return (
|
||||
isPinningDisappearingMessage &&
|
||||
seenPinMessageDisappearingMessagesWarningCount <= 3
|
||||
);
|
||||
}, [
|
||||
isPinningDisappearingMessage,
|
||||
seenPinMessageDisappearingMessagesWarningCount,
|
||||
]);
|
||||
|
||||
const [duration, setDuration] = useState<DurationOption | null>(null);
|
||||
const [confirmedReplaceOldestPin, setConfirmedReplaceOldestPin] =
|
||||
useState(false);
|
||||
|
||||
@@ -54,132 +87,232 @@ export const PinMessageDialog = memo(function PinMessageDialog(
|
||||
(open: boolean) => {
|
||||
onOpenChange(open);
|
||||
// reset state
|
||||
setDuration(null);
|
||||
setConfirmedReplaceOldestPin(false);
|
||||
setDuration(DurationOption.TIME_7_DAYS);
|
||||
},
|
||||
[onOpenChange]
|
||||
);
|
||||
|
||||
const handleConfirmReplaceOldestPin = useCallback((event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
const submit = useCallback(() => {
|
||||
strictAssert(
|
||||
duration != null,
|
||||
'Duration should not be null when submitting'
|
||||
);
|
||||
const durationValue = DURATION_OPTIONS[duration];
|
||||
onPinnedMessageAdd(messageId, durationValue);
|
||||
handleOpenChange(false);
|
||||
}, [onPinnedMessageAdd, messageId, duration, handleOpenChange]);
|
||||
|
||||
const handleConfirmReplaceOldestPin = useCallback(() => {
|
||||
setConfirmedReplaceOldestPin(true);
|
||||
}, []);
|
||||
|
||||
const handleValueChange = useCallback((value: string) => {
|
||||
const handleSelectDuration = useCallback(
|
||||
(selected: DurationOption) => {
|
||||
setDuration(selected);
|
||||
if (!needsConfirmDisappearingMessages) {
|
||||
submit();
|
||||
}
|
||||
},
|
||||
[needsConfirmDisappearingMessages, submit]
|
||||
);
|
||||
|
||||
const handleConfirmDisappearingMessages = useCallback(() => {
|
||||
onSeenPinMessageDisappearingMessagesWarning();
|
||||
submit();
|
||||
}, [onSeenPinMessageDisappearingMessagesWarning, submit]);
|
||||
|
||||
let step: Step;
|
||||
if (!props.open) {
|
||||
step = Step.CLOSED;
|
||||
} else if (needsConfirmReplaceOldestPin && !confirmedReplaceOldestPin) {
|
||||
step = Step.CONFIRM_REPLACE_OLDEST_PIN;
|
||||
} else if (duration == null) {
|
||||
step = Step.SELECT_PIN_DURATION;
|
||||
} else if (needsConfirmDisappearingMessages) {
|
||||
step = Step.DISAPPEARING_MESSAGES_WARNING;
|
||||
} else {
|
||||
step = Step.CLOSED;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PinMessageConfirmReplacePinDialog
|
||||
i18n={i18n}
|
||||
open={step === Step.CONFIRM_REPLACE_OLDEST_PIN}
|
||||
onOpenChange={handleOpenChange}
|
||||
onConfirmReplaceOldestPin={handleConfirmReplaceOldestPin}
|
||||
/>
|
||||
<PinMessageSelectDurationDialog
|
||||
i18n={i18n}
|
||||
open={step === Step.SELECT_PIN_DURATION}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSelectDuration={handleSelectDuration}
|
||||
/>
|
||||
<PinMessageDisappearingMessagesWarningDialog
|
||||
i18n={i18n}
|
||||
open={step === Step.DISAPPEARING_MESSAGES_WARNING}
|
||||
onOpenChange={handleOpenChange}
|
||||
onConfirm={handleConfirmDisappearingMessages}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function PinMessageConfirmReplacePinDialog(props: {
|
||||
i18n: LocalizerType;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirmReplaceOldestPin: () => void;
|
||||
}) {
|
||||
const { i18n, onConfirmReplaceOldestPin } = props;
|
||||
const handleConfirmReplaceOldestPin = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
onConfirmReplaceOldestPin();
|
||||
},
|
||||
[onConfirmReplaceOldestPin]
|
||||
);
|
||||
return (
|
||||
<AxoAlertDialog.Root open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
function PinMessageSelectDurationDialog(props: {
|
||||
i18n: LocalizerType;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSelectDuration: (duration: DurationOption) => void;
|
||||
}) {
|
||||
const { i18n, onOpenChange, onSelectDuration } = props;
|
||||
const [duration, setDuration] = useState(DurationOption.TIME_7_DAYS);
|
||||
|
||||
const handleDurationChange = useCallback((value: string) => {
|
||||
strictAssert(isValidDurationOption(value), `Invalid option: ${value}`);
|
||||
setDuration(value);
|
||||
}, []);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
handleOpenChange(false);
|
||||
}, [handleOpenChange]);
|
||||
onOpenChange(false);
|
||||
}, [onOpenChange]);
|
||||
|
||||
const handlePinnedMessageAdd = useCallback(() => {
|
||||
const durationValue = DURATION_OPTIONS[duration];
|
||||
onPinnedMessageAdd(messageId, durationValue);
|
||||
}, [duration, onPinnedMessageAdd, messageId]);
|
||||
|
||||
const showConfirmReplaceOldestPin =
|
||||
props.hasMaxPinnedMessages && !confirmedReplaceOldestPin;
|
||||
const handleConfirm = useCallback(() => {
|
||||
onSelectDuration(duration);
|
||||
}, [duration, onSelectDuration]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AxoAlertDialog.Root
|
||||
open={props.open && showConfirmReplaceOldestPin}
|
||||
onOpenChange={handleOpenChange}
|
||||
<AxoDialog.Root open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<AxoDialog.Content
|
||||
size="sm"
|
||||
escape="cancel-is-noop"
|
||||
disableMissingAriaDescriptionWarning
|
||||
>
|
||||
<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"
|
||||
disableMissingAriaDescriptionWarning
|
||||
>
|
||||
<AxoDialog.Header>
|
||||
<AxoDialog.Title>
|
||||
{i18n('icu:PinMessageDialog__Title')}
|
||||
</AxoDialog.Title>
|
||||
<AxoDialog.Close aria-label={i18n('icu:PinMessageDialog__Close')} />
|
||||
</AxoDialog.Header>
|
||||
<AxoDialog.Body>
|
||||
<AxoRadioGroup.Root
|
||||
value={duration}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
<AxoRadioGroup.Item value={DurationOption.TIME_24_HOURS}>
|
||||
<AxoDialog.Header>
|
||||
<AxoDialog.Title>
|
||||
{i18n('icu:PinMessageDialog__Title')}
|
||||
</AxoDialog.Title>
|
||||
<AxoDialog.Close aria-label={i18n('icu:PinMessageDialog__Close')} />
|
||||
</AxoDialog.Header>
|
||||
<AxoDialog.Body>
|
||||
<AxoRadioGroup.Root
|
||||
value={duration}
|
||||
onValueChange={handleDurationChange}
|
||||
>
|
||||
<AxoRadioGroup.Item value={DurationOption.TIME_24_HOURS}>
|
||||
<AxoRadioGroup.Indicator />
|
||||
<AxoRadioGroup.Label>
|
||||
{i18n('icu:PinMessageDialog__Option--TIME_24_HOURS')}
|
||||
</AxoRadioGroup.Label>
|
||||
</AxoRadioGroup.Item>
|
||||
<AxoRadioGroup.Item value={DurationOption.TIME_7_DAYS}>
|
||||
<AxoRadioGroup.Indicator />
|
||||
<AxoRadioGroup.Label>
|
||||
{i18n('icu:PinMessageDialog__Option--TIME_7_DAYS')}
|
||||
</AxoRadioGroup.Label>
|
||||
</AxoRadioGroup.Item>
|
||||
<AxoRadioGroup.Item value={DurationOption.TIME_30_DAYS}>
|
||||
<AxoRadioGroup.Indicator />
|
||||
<AxoRadioGroup.Label>
|
||||
{i18n('icu:PinMessageDialog__Option--TIME_30_DAYS')}
|
||||
</AxoRadioGroup.Label>
|
||||
</AxoRadioGroup.Item>
|
||||
<AxoRadioGroup.Item value={DurationOption.FOREVER}>
|
||||
<AxoRadioGroup.Indicator />
|
||||
<AxoRadioGroup.Label>
|
||||
{i18n('icu:PinMessageDialog__Option--FOREVER')}
|
||||
</AxoRadioGroup.Label>
|
||||
</AxoRadioGroup.Item>
|
||||
{isInternalFeaturesEnabled() && (
|
||||
<AxoRadioGroup.Item value={DurationOption.DEBUG_10_SECONDS}>
|
||||
<AxoRadioGroup.Indicator />
|
||||
<AxoRadioGroup.Label>
|
||||
{i18n('icu:PinMessageDialog__Option--TIME_24_HOURS')}
|
||||
</AxoRadioGroup.Label>
|
||||
<AxoRadioGroup.Label>10 seconds (Internal)</AxoRadioGroup.Label>
|
||||
</AxoRadioGroup.Item>
|
||||
<AxoRadioGroup.Item value={DurationOption.TIME_7_DAYS}>
|
||||
<AxoRadioGroup.Indicator />
|
||||
<AxoRadioGroup.Label>
|
||||
{i18n('icu:PinMessageDialog__Option--TIME_7_DAYS')}
|
||||
</AxoRadioGroup.Label>
|
||||
</AxoRadioGroup.Item>
|
||||
<AxoRadioGroup.Item value={DurationOption.TIME_30_DAYS}>
|
||||
<AxoRadioGroup.Indicator />
|
||||
<AxoRadioGroup.Label>
|
||||
{i18n('icu:PinMessageDialog__Option--TIME_30_DAYS')}
|
||||
</AxoRadioGroup.Label>
|
||||
</AxoRadioGroup.Item>
|
||||
<AxoRadioGroup.Item value={DurationOption.FOREVER}>
|
||||
<AxoRadioGroup.Indicator />
|
||||
<AxoRadioGroup.Label>
|
||||
{i18n('icu:PinMessageDialog__Option--FOREVER')}
|
||||
</AxoRadioGroup.Label>
|
||||
</AxoRadioGroup.Item>
|
||||
{isInternalFeaturesEnabled() && (
|
||||
<AxoRadioGroup.Item value={DurationOption.DEBUG_10_SECONDS}>
|
||||
<AxoRadioGroup.Indicator />
|
||||
<AxoRadioGroup.Label>
|
||||
10 seconds (Internal)
|
||||
</AxoRadioGroup.Label>
|
||||
</AxoRadioGroup.Item>
|
||||
)}
|
||||
</AxoRadioGroup.Root>
|
||||
</AxoDialog.Body>
|
||||
<AxoDialog.Footer>
|
||||
<AxoDialog.Actions>
|
||||
<AxoDialog.Action variant="secondary" onClick={handleCancel}>
|
||||
{i18n('icu:PinMessageDialog__Cancel')}
|
||||
</AxoDialog.Action>
|
||||
<AxoDialog.Action
|
||||
variant="primary"
|
||||
onClick={handlePinnedMessageAdd}
|
||||
>
|
||||
{i18n('icu:PinMessageDialog__Pin')}
|
||||
</AxoDialog.Action>
|
||||
</AxoDialog.Actions>
|
||||
</AxoDialog.Footer>
|
||||
</AxoDialog.Content>
|
||||
</AxoDialog.Root>
|
||||
</>
|
||||
)}
|
||||
</AxoRadioGroup.Root>
|
||||
</AxoDialog.Body>
|
||||
<AxoDialog.Footer>
|
||||
<AxoDialog.Actions>
|
||||
<AxoDialog.Action variant="secondary" onClick={handleCancel}>
|
||||
{i18n('icu:PinMessageDialog__Cancel')}
|
||||
</AxoDialog.Action>
|
||||
<AxoDialog.Action variant="primary" onClick={handleConfirm}>
|
||||
{i18n('icu:PinMessageDialog__Pin')}
|
||||
</AxoDialog.Action>
|
||||
</AxoDialog.Actions>
|
||||
</AxoDialog.Footer>
|
||||
</AxoDialog.Content>
|
||||
</AxoDialog.Root>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function PinMessageDisappearingMessagesWarningDialog(props: {
|
||||
i18n: LocalizerType;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: () => void;
|
||||
}) {
|
||||
const { i18n } = props;
|
||||
return (
|
||||
<AxoAlertDialog.Root open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<AxoAlertDialog.Content escape="cancel-is-destructive">
|
||||
<AxoAlertDialog.Body>
|
||||
<AxoAlertDialog.Title>
|
||||
{i18n('icu:PinMessageDisappearingMessagesWarningDialog__Title')}
|
||||
</AxoAlertDialog.Title>
|
||||
<AxoAlertDialog.Description>
|
||||
{i18n(
|
||||
'icu:PinMessageDisappearingMessagesWarningDialog__Description'
|
||||
)}
|
||||
</AxoAlertDialog.Description>
|
||||
</AxoAlertDialog.Body>
|
||||
<AxoAlertDialog.Footer>
|
||||
<AxoAlertDialog.Action variant="primary" onClick={props.onConfirm}>
|
||||
{i18n('icu:PinMessageDisappearingMessagesWarningDialog__Okay')}
|
||||
</AxoAlertDialog.Action>
|
||||
</AxoAlertDialog.Footer>
|
||||
</AxoAlertDialog.Content>
|
||||
</AxoAlertDialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import type { MessageForwardDraft } from '../../types/ForwardDraft.std.js';
|
||||
import { hydrateRanges } from '../../util/BodyRange.node.js';
|
||||
import {
|
||||
getConversationSelector,
|
||||
getHasMaxPinnedMessages,
|
||||
type GetConversationByIdType,
|
||||
} from '../selectors/conversations.dom.js';
|
||||
import { missingCaseError } from '../../util/missingCaseError.std.js';
|
||||
@@ -62,6 +63,8 @@ import type { DataPropsType as TapToViewNotAvailablePropsType } from '../../comp
|
||||
import type { DataPropsType as BackfillFailureModalPropsType } from '../../components/BackfillFailureModal.dom.js';
|
||||
import type { SmartDraftGifMessageSendModalProps } from '../smart/DraftGifMessageSendModal.preload.js';
|
||||
import { onCriticalIdlePrimaryDeviceModalDismissed } from '../../util/handleServerAlerts.preload.js';
|
||||
import type { PinMessageDialogData } from '../smart/PinMessageDialog.preload.js';
|
||||
import type { StateThunk } from '../types.std.js';
|
||||
|
||||
const log = createLogger('globalModals');
|
||||
|
||||
@@ -146,6 +149,7 @@ export type GlobalModalsStateType = ReadonlyDeep<{
|
||||
} | null;
|
||||
messageRequestActionsConfirmationProps: MessageRequestActionsConfirmationPropsType | null;
|
||||
notePreviewModalProps: NotePreviewModalPropsType | null;
|
||||
pinMessageDialogData: PinMessageDialogData | null;
|
||||
usernameOnboardingState: UsernameOnboardingState;
|
||||
mediaPermissionsModalProps?: {
|
||||
mediaType: 'camera' | 'microphone';
|
||||
@@ -233,6 +237,7 @@ const SHOW_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL =
|
||||
'globalModals/SHOW_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL';
|
||||
const HIDE_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL =
|
||||
'globalModals/HIDE_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL';
|
||||
const TOGGLE_PIN_MESSAGE_DIALOG = 'globalModals/TOGGLE_PIN_MESSAGE_DIALOG';
|
||||
|
||||
export type ContactModalStateType = ReadonlyDeep<{
|
||||
contactId: string;
|
||||
@@ -500,6 +505,11 @@ type CloseEditHistoryModalActionType = ReadonlyDeep<{
|
||||
type: typeof CLOSE_EDIT_HISTORY_MODAL;
|
||||
}>;
|
||||
|
||||
type TogglePinMessageDialogActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_PIN_MESSAGE_DIALOG;
|
||||
payload: PinMessageDialogData | null;
|
||||
}>;
|
||||
|
||||
export type GlobalModalsActionType = ReadonlyDeep<
|
||||
| CloseEditHistoryModalActionType
|
||||
| CloseDebugLogErrorModalActionType
|
||||
@@ -555,6 +565,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
|
||||
| ToggleSafetyNumberModalActionType
|
||||
| ToggleSignalConnectionsModalActionType
|
||||
| ToggleUsernameOnboardingActionType
|
||||
| TogglePinMessageDialogActionType
|
||||
>;
|
||||
|
||||
// Action Creators
|
||||
@@ -612,6 +623,8 @@ export const actions = {
|
||||
toggleSafetyNumberModal,
|
||||
toggleSignalConnectionsModal,
|
||||
toggleUsernameOnboarding,
|
||||
showPinMessageDialog,
|
||||
hidePinMessageDialog,
|
||||
};
|
||||
|
||||
export const useGlobalModalActions = (): BoundActionCreatorsMapObject<
|
||||
@@ -1342,6 +1355,30 @@ function copyOverMessageAttributesIntoForwardMessages(
|
||||
});
|
||||
}
|
||||
|
||||
function showPinMessageDialog(
|
||||
messageId: string,
|
||||
isPinningDisappearingMessage: boolean
|
||||
): StateThunk<TogglePinMessageDialogActionType> {
|
||||
return async (dispatch, getState) => {
|
||||
const hasMaxPinnedMessages = getHasMaxPinnedMessages(getState());
|
||||
dispatch({
|
||||
type: TOGGLE_PIN_MESSAGE_DIALOG,
|
||||
payload: {
|
||||
messageId,
|
||||
hasMaxPinnedMessages,
|
||||
isPinningDisappearingMessage,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function hidePinMessageDialog(): TogglePinMessageDialogActionType {
|
||||
return {
|
||||
type: TOGGLE_PIN_MESSAGE_DIALOG,
|
||||
payload: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Reducer
|
||||
|
||||
export function getEmptyState(): GlobalModalsStateType {
|
||||
@@ -1367,6 +1404,7 @@ export function getEmptyState(): GlobalModalsStateType {
|
||||
messageRequestActionsConfirmationProps: null,
|
||||
tapToViewNotAvailableModalProps: undefined,
|
||||
notePreviewModalProps: null,
|
||||
pinMessageDialogData: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1812,5 +1850,12 @@ export function reducer(
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_PIN_MESSAGE_DIALOG) {
|
||||
return {
|
||||
...state,
|
||||
pinMessageDialogData: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ import type { AllChatFoldersMutedStats } from '../../util/countMutedStats.std.js
|
||||
import { countAllChatFoldersMutedStats } from '../../util/countMutedStats.std.js';
|
||||
import { getActiveProfile } from './notificationProfiles.dom.js';
|
||||
import type { PinnedMessage } from '../../types/PinnedMessage.std.js';
|
||||
import { getPinnedMessagesLimit } from '../../util/pinnedMessages.dom.js';
|
||||
|
||||
const { isNumber, pick } = lodash;
|
||||
|
||||
@@ -338,6 +339,15 @@ export const getPinnedMessagesMessageIds: StateSelector<ReadonlyArray<string>> =
|
||||
});
|
||||
});
|
||||
|
||||
export const getHasMaxPinnedMessages: StateSelector<boolean> = createSelector(
|
||||
getPinnedMessages,
|
||||
pinnedMessages => {
|
||||
const pinnedMessagesLimit = getPinnedMessagesLimit();
|
||||
const pinnedMessagesCount = pinnedMessages.length;
|
||||
return pinnedMessagesCount >= pinnedMessagesLimit;
|
||||
}
|
||||
);
|
||||
|
||||
const collator = new Intl.Collator();
|
||||
|
||||
// Note: we will probably want to put i18n and regionCode back when we are formatting
|
||||
|
||||
@@ -6,6 +6,8 @@ import { createSelector } from 'reselect';
|
||||
import type { StateType } from '../reducer.preload.js';
|
||||
import type { GlobalModalsStateType } from '../ducks/globalModals.preload.js';
|
||||
import { UsernameOnboardingState } from '../../types/globalModals.std.js';
|
||||
import type { StateSelector } from '../types.std.js';
|
||||
import type { PinMessageDialogData } from '../smart/PinMessageDialog.preload.js';
|
||||
|
||||
export const getGlobalModalsState = (state: StateType): GlobalModalsStateType =>
|
||||
state.globalModals;
|
||||
@@ -92,3 +94,9 @@ export const getNotePreviewModalProps = createSelector(
|
||||
getGlobalModalsState,
|
||||
({ notePreviewModalProps }) => notePreviewModalProps
|
||||
);
|
||||
|
||||
export const getPinMessageDialogData: StateSelector<PinMessageDialogData | null> =
|
||||
createSelector(
|
||||
getGlobalModalsState,
|
||||
({ pinMessageDialogData }) => pinMessageDialogData
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
isValidEmojiSkinTone,
|
||||
} from '../../components/fun/data/emojis.std.js';
|
||||
import { BackupLevel } from '../../services/backups/types.std.js';
|
||||
import type { StateSelector } from '../types.std.js';
|
||||
|
||||
const DEFAULT_PREFERRED_LEFT_PANE_WIDTH = 320;
|
||||
|
||||
@@ -307,3 +308,9 @@ export const getServerAlerts = createSelector(
|
||||
getItems,
|
||||
(state: ItemsStateType) => state.serverAlerts ?? {}
|
||||
);
|
||||
|
||||
export const getSeenPinMessageDisappearingMessagesWarningCount: StateSelector<number> =
|
||||
createSelector(
|
||||
getItems,
|
||||
state => state.seenPinMessageDisappearingMessagesWarningCount ?? 0
|
||||
);
|
||||
|
||||
@@ -170,7 +170,6 @@ import type { MessageRequestResponseNotificationData } from '../../components/co
|
||||
import type { PinnedMessageNotificationData } from '../../components/conversation/pinned-messages/PinnedMessageNotification.dom.js';
|
||||
import { canEditGroupInfo } from '../../util/canEditGroupInfo.preload.js';
|
||||
import { isPinnedMessagesSendEnabled } from '../../util/isPinnedMessagesEnabled.dom.js';
|
||||
import { getPinnedMessagesLimit } from '../../util/pinnedMessages.dom.js';
|
||||
|
||||
const { groupBy, isEmpty, isNumber, isObject, map } = lodash;
|
||||
|
||||
@@ -967,9 +966,6 @@ export const getPropsForMessage = (
|
||||
expirationStartTimestamp,
|
||||
}),
|
||||
giftBadge: message.giftBadge,
|
||||
hasMaxPinnedMessages: getHasMaxPinnedMessages(
|
||||
options.pinnedMessagesMessageIds ?? []
|
||||
),
|
||||
poll: getPollForMessage(message, {
|
||||
conversationSelector: options.conversationSelector,
|
||||
ourConversationId,
|
||||
@@ -2427,14 +2423,6 @@ export function canPinMessage(
|
||||
return true;
|
||||
}
|
||||
|
||||
function getHasMaxPinnedMessages(
|
||||
pinnedMessagesMessageIds: ReadonlyArray<string>
|
||||
) {
|
||||
const pinnedMessagesLimit = getPinnedMessagesLimit();
|
||||
const pinnedMessagesCount = pinnedMessagesMessageIds.length;
|
||||
return pinnedMessagesCount >= pinnedMessagesLimit;
|
||||
}
|
||||
|
||||
export function getLastChallengeError(
|
||||
message: Pick<MessageWithUIFieldsType, 'errors'>
|
||||
): ShallowChallengeError | undefined {
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
shouldShowPlaintextWorkflow,
|
||||
shouldShowLocalBackupWorkflow,
|
||||
} from '../selectors/backups.std.js';
|
||||
import { SmartPinMessageDialog } from './PinMessageDialog.preload.js';
|
||||
|
||||
function renderCallLinkAddNameModal(): React.JSX.Element {
|
||||
return <SmartCallLinkAddNameModal />;
|
||||
@@ -100,6 +101,10 @@ function renderNotePreviewModal(): React.JSX.Element {
|
||||
return <SmartNotePreviewModal />;
|
||||
}
|
||||
|
||||
function renderPinMessageDialog(): React.JSX.Element {
|
||||
return <SmartPinMessageDialog />;
|
||||
}
|
||||
|
||||
function renderPlaintextExportWorkflow(): React.JSX.Element {
|
||||
return <SmartPlaintextExportWorkflow />;
|
||||
}
|
||||
@@ -160,6 +165,7 @@ export const SmartGlobalModalContainer = memo(
|
||||
mediaPermissionsModalProps,
|
||||
messageRequestActionsConfirmationProps,
|
||||
notePreviewModalProps,
|
||||
pinMessageDialogData,
|
||||
isProfileNameWarningModalVisible,
|
||||
profileNameWarningModalConversationType,
|
||||
isShortcutGuideModalVisible,
|
||||
@@ -278,6 +284,7 @@ export const SmartGlobalModalContainer = memo(
|
||||
closeMediaPermissionsModal={closeMediaPermissionsModal}
|
||||
openSystemMediaPermissions={window.IPC.openSystemMediaPermissions}
|
||||
notePreviewModalProps={notePreviewModalProps}
|
||||
pinMessageDialogData={pinMessageDialogData}
|
||||
hasSafetyNumberChangeModal={hasSafetyNumberChangeModal}
|
||||
hideBackfillFailureModal={hideBackfillFailureModal}
|
||||
hideUserNotFoundModal={hideUserNotFoundModal}
|
||||
@@ -310,6 +317,7 @@ export const SmartGlobalModalContainer = memo(
|
||||
renderMessageRequestActionsConfirmation
|
||||
}
|
||||
renderNotePreviewModal={renderNotePreviewModal}
|
||||
renderPinMessageDialog={renderPinMessageDialog}
|
||||
renderPlaintextExportWorkflow={renderPlaintextExportWorkflow}
|
||||
renderLocalBackupExportWorkflow={renderLocalBackupExportWorkflow}
|
||||
renderProfileNameWarningModal={renderProfileNameWarningModal}
|
||||
|
||||
64
ts/state/smart/PinMessageDialog.preload.tsx
Normal file
64
ts/state/smart/PinMessageDialog.preload.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getIntl } from '../selectors/user.std.js';
|
||||
import { useConversationsActions } from '../ducks/conversations.preload.js';
|
||||
import { PinMessageDialog } from '../../components/conversation/pinned-messages/PinMessageDialog.dom.js';
|
||||
import { useItemsActions } from '../ducks/items.preload.js';
|
||||
import { getSeenPinMessageDisappearingMessagesWarningCount } from '../selectors/items.dom.js';
|
||||
import { getPinMessageDialogData } from '../selectors/globalModals.std.js';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals.preload.js';
|
||||
|
||||
export type PinMessageDialogData = Readonly<{
|
||||
messageId: string;
|
||||
hasMaxPinnedMessages: boolean;
|
||||
isPinningDisappearingMessage: boolean;
|
||||
}>;
|
||||
|
||||
export const SmartPinMessageDialog = memo(function SmartPinMessageDialog() {
|
||||
const i18n = useSelector(getIntl);
|
||||
const pinMessageDialogData = useSelector(getPinMessageDialogData);
|
||||
const { hidePinMessageDialog } = useGlobalModalActions();
|
||||
const { onPinnedMessageAdd } = useConversationsActions();
|
||||
|
||||
const seenPinMessageDisappearingMessagesWarningCount = useSelector(
|
||||
getSeenPinMessageDisappearingMessagesWarningCount
|
||||
);
|
||||
const { putItem } = useItemsActions();
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
hidePinMessageDialog();
|
||||
}, [hidePinMessageDialog]);
|
||||
|
||||
const handleSeenPinMessageDisappearingMessagesWarning = useCallback(() => {
|
||||
putItem(
|
||||
'seenPinMessageDisappearingMessagesWarningCount',
|
||||
seenPinMessageDisappearingMessagesWarningCount + 1
|
||||
);
|
||||
}, [putItem, seenPinMessageDisappearingMessagesWarningCount]);
|
||||
|
||||
if (pinMessageDialogData == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PinMessageDialog
|
||||
i18n={i18n}
|
||||
open
|
||||
onOpenChange={handleClose}
|
||||
messageId={pinMessageDialogData.messageId}
|
||||
hasMaxPinnedMessages={pinMessageDialogData.hasMaxPinnedMessages}
|
||||
isPinningDisappearingMessage={
|
||||
pinMessageDialogData.isPinningDisappearingMessage
|
||||
}
|
||||
seenPinMessageDisappearingMessagesWarningCount={
|
||||
seenPinMessageDisappearingMessagesWarningCount
|
||||
}
|
||||
onSeenPinMessageDisappearingMessagesWarning={
|
||||
handleSeenPinMessageDisappearingMessagesWarning
|
||||
}
|
||||
onPinnedMessageAdd={onPinnedMessageAdd}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -131,7 +131,6 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
messageExpanded,
|
||||
onPinnedMessageAdd,
|
||||
onPinnedMessageRemove,
|
||||
openGiftBadge,
|
||||
pushPanelForConversation,
|
||||
@@ -164,6 +163,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
||||
const {
|
||||
showContactModal,
|
||||
showEditHistoryModal,
|
||||
showPinMessageDialog,
|
||||
showTapToViewNotAvailableModal,
|
||||
toggleMessageRequestActionsConfirmation,
|
||||
toggleDeleteMessagesModal,
|
||||
@@ -239,7 +239,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
||||
}
|
||||
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
|
||||
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
||||
onPinnedMessageAdd={onPinnedMessageAdd}
|
||||
showPinMessageDialog={showPinMessageDialog}
|
||||
onPinnedMessageRemove={onPinnedMessageRemove}
|
||||
scrollToPinnedMessage={scrollToPinnedMessage}
|
||||
retryDeleteForEveryone={retryDeleteForEveryone}
|
||||
|
||||
1
ts/types/Storage.d.ts
vendored
1
ts/types/Storage.d.ts
vendored
@@ -117,6 +117,7 @@ export type StorageAccessType = {
|
||||
sessionResets: SessionResetsType;
|
||||
showStickerPickerHint: boolean;
|
||||
showStickersIntroduction: boolean;
|
||||
seenPinMessageDisappearingMessagesWarningCount: number;
|
||||
signedKeyId: number;
|
||||
signedKeyIdPNI: number;
|
||||
signedKeyUpdateTime: number;
|
||||
|
||||
Reference in New Issue
Block a user