diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6459b80bb7..e49ec59a6a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -8216,6 +8216,14 @@ "messageformat": "Advanced", "description": "Title for advanced settings" }, + "icu:Preferences--change-signal-pin": { + "messageformat": "Change your PIN", + "description": "Title for changing Signal PIN in settings" + }, + "icu:Preferences--change-signal-pin-button": { + "messageformat": "Change", + "description": "Button for changing Signal PIN in settings" + }, "icu:Preferences--signal-pin": { "messageformat": "Signal PIN", "description": "Title for Signal PIN settings" @@ -10910,6 +10918,10 @@ "messageformat": "Incorrect PIN. Try again", "description": "Error text shown in PIN input box in Signal PIN reminder dialog, when the input was incorrect" }, + "icu:PinReminderModal__forgot": { + "messageformat": "Forgot PIN", + "description": "Forgot PIN button in Signal PIN reminder dialog" + }, "icu:PinReminderModal__confirm": { "messageformat": "Confirm", "description": "Confirm button in Signal PIN reminder dialog" @@ -10918,6 +10930,46 @@ "messageformat": "PIN correct. We’ll remind you again later.", "description": "Toast message when Signal PIN reminder was completed successfully." }, + "icu:PinChangeModal__title--create": { + "messageformat": "Change your Signal PIN", + "description": "Title of Signal PIN change dialog when creating a new PIN" + }, + "icu:PinChangeModal__body--create": { + "messageformat": "You can change your PIN as long as this device is registered. Learn more", + "description": "Body of Signal PIN change dialog when creating a new PIN" + }, + "icu:PinChangeModal__title--confirm": { + "messageformat": "Confirm your Signal PIN", + "description": "Title of Signal PIN change dialog when confirming a new PIN" + }, + "icu:PinChangeModal__body--confirm": { + "messageformat": "Enter the PIN you just created.", + "description": "Body of Signal PIN change dialog when confirming a new PIN" + }, + "icu:PinChangeModal__pin-input-placeholder--create": { + "messageformat": "Enter a new PIN", + "description": "Placeholder text of PIN input box in Signal PIN change dialog when creating a new PIN" + }, + "icu:PinChangeModal__pin-input-placeholder--confirm": { + "messageformat": "Confirm PIN", + "description": "Placeholder text of PIN input box in Signal PIN change dialog when confirming PIN" + }, + "icu:PinChangeModal__cancel": { + "messageformat": "Cancel", + "description": "Cancel button in Signal PIN change dialog" + }, + "icu:PinChangeModal__continue": { + "messageformat": "Continue", + "description": "Continue button in Signal PIN change dialog, to go to the Confirm step" + }, + "icu:PinChangeModal__confirm": { + "messageformat": "Confirm", + "description": "Confirm button in Signal PIN change dialog" + }, + "icu:PinChangeCompletedToast": { + "messageformat": "New PIN created.", + "description": "Toast message when Signal PIN was changed successfully." + }, "icu:UnsupportedOSWarningDialog__body": { "messageformat": "Signal Desktop will no longer support your computer’s version of {OS} soon. To keep using Signal, update your computer’s operating system by {expirationDate}. Learn more", "description": "Body of a dialog displayed on unsupported operating systems" diff --git a/ts/components/GlobalModalContainer.dom.tsx b/ts/components/GlobalModalContainer.dom.tsx index 82360cf1c9..6c2378d29e 100644 --- a/ts/components/GlobalModalContainer.dom.tsx +++ b/ts/components/GlobalModalContainer.dom.tsx @@ -181,6 +181,9 @@ export type PropsType = { // TerminateGroupFailedModal terminateGroupFailedModal: { conversationId: string } | null; renderTerminateGroupFailedModal: () => JSX.Element | null; + // PinChangeModal + isPinChangeModalVisible: boolean; + renderPinChangeModal: () => JSX.Element | null; // PinReminderModal pinReminderState: PinReminderState; renderPinReminderModal: () => JSX.Element | null; @@ -249,6 +252,9 @@ export function GlobalModalContainer({ // PinMessageDialog pinMessageDialogData, renderPinMessageDialog, + // PinChangeModal + isPinChangeModalVisible, + renderPinChangeModal, // PinReminderModal pinReminderState, renderPinReminderModal, @@ -420,6 +426,12 @@ export function GlobalModalContainer({ return renderPinMessageDialog(); } + // PIN change renders with precedence over the reminder modal, since + // the reminder modal can open the PIN change modal. + if (isPinChangeModalVisible) { + return renderPinChangeModal(); + } + if (pinReminderState === PinReminderState.Modal) { return renderPinReminderModal(); } diff --git a/ts/components/PinChangeModal.dom.stories.tsx b/ts/components/PinChangeModal.dom.stories.tsx new file mode 100644 index 0000000000..fefec2ff71 --- /dev/null +++ b/ts/components/PinChangeModal.dom.stories.tsx @@ -0,0 +1,24 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { JSX } from 'react'; + +import { action } from '@storybook/addon-actions'; +import { type Meta } from '@storybook/react'; +import { PinChangeModal } from './PinChangeModal.dom.tsx'; + +const { i18n } = window.SignalContext; + +export default { + title: 'Components/PinChangeModal', +} satisfies Meta; + +export function Default(): JSX.Element { + return ( + + ); +} diff --git a/ts/components/PinChangeModal.dom.tsx b/ts/components/PinChangeModal.dom.tsx new file mode 100644 index 0000000000..d06ef5e4d1 --- /dev/null +++ b/ts/components/PinChangeModal.dom.tsx @@ -0,0 +1,184 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +import { useCallback, useState, type JSX } from 'react'; + +import type { LocalizerType } from '../types/Util.std.ts'; +import { AxoDialog } from '../axo/AxoDialog.dom.tsx'; +import { tw } from '../axo/tw.dom.tsx'; +import { AxoPasswordField } from '../axo/fields/AxoPasswordField.dom.tsx'; +import { + InputContainer, + PIN_ARTICLE_ON_SUPPORT, + Spacer, +} from './standaloneRegistration/util/StepComponents.dom.tsx'; +import { + PIN_LENGTH_MINIMUM, + PIN_MAX_BYTES, + PIN_MAX_GRAPHEMES, +} from './standaloneRegistration/stages/VerifyPIN.dom.tsx'; +import { missingCaseError } from '../util/missingCaseError.std.ts'; +import { I18n } from './I18n.dom.tsx'; + +const learnMoreLink = (parts: Array) => ( + + {parts} + +); + +export function PinChangeModal({ + i18n, + onCancel, + onSubmit, +}: { + i18n: LocalizerType; + onCancel: () => void; + onSubmit: (pin: string) => void; +}): JSX.Element { + const [pin, setPin] = useState(''); + const [stagedPin, setStagedPin] = useState(''); + const [isValidPIN, setIsValidPIN] = useState(false); + const [step, setStep] = useState<'create' | 'confirm'>('create'); + + const handlePinInputChange = useCallback( + (value: string) => { + setPin(value); + + const isValid = + value.length >= PIN_LENGTH_MINIMUM && + (step === 'confirm' ? value === stagedPin : true); + setIsValidPIN(isValid); + }, + [stagedPin, step, setIsValidPIN, setPin] + ); + + const handleCancel = useCallback(() => { + if (step === 'create') { + onCancel(); + } else if (step === 'confirm') { + setPin(stagedPin); + setStagedPin(''); + setIsValidPIN(true); + setStep('create'); + } else { + throw missingCaseError(step); + } + }, [stagedPin, step, onCancel, setStep]); + + const handleSubmit = useCallback(() => { + if (!isValidPIN) { + return; + } + + if (step === 'create') { + setStagedPin(pin); + setPin(''); + setIsValidPIN(false); + setStep('confirm'); + } else if (step === 'confirm') { + onSubmit(pin); + } else { + throw missingCaseError(step); + } + }, [isValidPIN, pin, step, onSubmit]); + + return ( + { + if (!isOpen) { + onCancel(); + } + }} + > + + { + event.preventDefault(); + handleSubmit(); + }} + > + + {step === 'confirm' && } + + {step === 'create' + ? i18n('icu:PinChangeModal__title--create') + : i18n('icu:PinChangeModal__title--confirm')} + + + + + + + {step === 'create' ? ( + + ) : ( + + )} + + }> + + + + + + + + {i18n('icu:PinChangeModal__cancel')} + + + {step === 'create' + ? i18n('icu:PinChangeModal__continue') + : i18n('icu:PinChangeModal__confirm')} + + {/* This is used so the Enter key submits the form. */} + {/* oxlint-disable-next-line jsx-a11y/control-has-associated-label */} + + + + + + + ); +} diff --git a/ts/components/PinReminderModal.dom.stories.tsx b/ts/components/PinReminderModal.dom.stories.tsx index eda39d6036..e43027315d 100644 --- a/ts/components/PinReminderModal.dom.stories.tsx +++ b/ts/components/PinReminderModal.dom.stories.tsx @@ -18,6 +18,7 @@ export function Default(): JSX.Element { Math.random() > 0.5} i18n={i18n} /> @@ -30,6 +31,7 @@ export function WrongPin(): JSX.Element { internalHasValidationError open onCancel={action('onCancel')} + onForgotPin={action('onForgotPin')} onPinEntry={() => Math.random() > 0.5} i18n={i18n} /> diff --git a/ts/components/PinReminderModal.dom.tsx b/ts/components/PinReminderModal.dom.tsx index 5764276e40..c0c3dc07bc 100644 --- a/ts/components/PinReminderModal.dom.tsx +++ b/ts/components/PinReminderModal.dom.tsx @@ -21,12 +21,14 @@ export function PinReminderModal({ internalHasValidationError, open, onCancel, + onForgotPin, onPinEntry, }: { internalHasValidationError?: boolean; i18n: LocalizerType; open: boolean; onCancel: () => void; + onForgotPin: () => void; onPinEntry: (pin: string, ignoreWrongGuess?: boolean) => boolean; }): JSX.Element { const [pin, setPin] = useState(''); @@ -124,6 +126,12 @@ export function PinReminderModal({ + + {i18n('icu:PinReminderModal__forgot')} + unknown; setSettingsLocation: (settingsLocation: SettingsLocation) => unknown; + showPinChangeModal: () => void; showToast: (toast: AnyToast) => unknown; startLocalBackupExport: () => void; startPlaintextExport: () => unknown; @@ -616,6 +617,7 @@ export function Preferences({ setGlobalDefaultConversationColor, setSettingsLocation, shouldShowUpdateDialog, + showPinChangeModal, showToast, startLocalBackupExport, startPlaintextExport, @@ -888,6 +890,17 @@ export function Preferences({ {weArePrimaryDevice && ( + + {i18n('icu:Preferences--change-signal-pin-button')} + + } + /> {i18n('icu:PinChangeCompletedToast')} + ); + } + if (toastType === ToastType.PinReminderCompleted) { return ( {i18n('icu:PinReminderCompletedToast')} diff --git a/ts/services/pinReminder.preload.ts b/ts/services/pinReminder.preload.ts index 7fb07caa45..000db4b189 100644 --- a/ts/services/pinReminder.preload.ts +++ b/ts/services/pinReminder.preload.ts @@ -91,6 +91,12 @@ class PinReminderService { } } + async resetPinReminderTimes(): Promise { + await itemStorage.put(STORAGE_KEY_LAST_REMINDER_TIME, Date.now()); + await itemStorage.put(STORAGE_KEY_NEXT_INTERVAL, this.#defaultInterval); + window.reduxActions.globalModals.togglePinReminder(PinReminderState.None); + } + handleSkipReminder(): void { drop(this.#resolveReminder('skip')); } diff --git a/ts/state/ducks/globalModals.preload.ts b/ts/state/ducks/globalModals.preload.ts index 9265d37d1b..2fa4e72664 100644 --- a/ts/state/ducks/globalModals.preload.ts +++ b/ts/state/ducks/globalModals.preload.ts @@ -4,6 +4,7 @@ import type { CallSummary } from '@signalapp/ringrtc'; import type { ThunkAction } from 'redux-thunk'; import type { ReadonlyDeep } from 'type-fest'; +import { v7 as generateUuid } from 'uuid'; import OS from '../../util/os/osMain.node.ts'; import type { ExplodePromiseResultType } from '../../util/explodePromise.std.ts'; import type { @@ -71,6 +72,12 @@ import type { ErrorModalDataProps } from '../../components/ErrorModal.dom.tsx'; import { isDownloadableOrBackfillable } from '../../util/downloadAttachment.preload.ts'; import { backupsService } from '../../services/backups/index.preload.ts'; import { getHasMediaBackups } from '../selectors/items.dom.ts'; +import { registrationJobQueue } from '../../jobs/registrationJobQueue.preload.ts'; +import { toLogFormat } from '../../types/errors.std.ts'; +import { pinReminderService } from '../../services/pinReminder.preload.ts'; +import { drop } from '../../util/drop.std.ts'; +import { showToast, type ToastActionType } from './toast.preload.ts'; +import { ToastType } from '../../types/Toast.dom.tsx'; const log = createLogger('globalModals'); @@ -150,6 +157,7 @@ export type GlobalModalsStateType = ReadonlyDeep<{ gv2MigrationProps?: MigrateToGV2PropsType; groupMemberLabelInfoModalState?: GroupMemberLabelInfoPropsType; hasConfirmationModal: boolean; + isPinChangeModalVisible: boolean; isProfileNameWarningModalVisible: boolean; profileNameWarningModalConversationType?: string; isShortcutGuideModalVisible: boolean; @@ -227,6 +235,8 @@ const TOGGLE_CALL_LINK_PENDING_PARTICIPANT_MODAL = export const SHOW_CALL_QUALITY_SURVEY = 'globalModals/SHOW_CALL_QUALITY_SURVEY'; export const HIDE_CALL_QUALITY_SURVEY = 'globalModals/HIDE_CALL_QUALITY_SURVEY'; const TOGGLE_ABOUT_MODAL = 'globalModals/TOGGLE_ABOUT_MODAL'; +const SHOW_PIN_CHANGE_MODAL = 'globalModals/SHOW_PIN_CHANGE_MODAL'; +const HIDE_PIN_CHANGE_MODAL = 'globalModals/HIDE_PIN_CHANGE_MODAL'; const TOGGLE_PIN_REMINDER = 'globalModals/TOGGLE_PIN_REMINDER'; const TOGGLE_SIGNAL_CONNECTIONS_MODAL = 'globalModals/TOGGLE_SIGNAL_CONNECTIONS_MODAL'; @@ -560,6 +570,14 @@ type TogglePinMessageDialogActionType = ReadonlyDeep<{ payload: PinMessageDialogData | null; }>; +type HidePinChangeModalActionType = ReadonlyDeep<{ + type: typeof HIDE_PIN_CHANGE_MODAL; +}>; + +type ShowPinChangeModalActionType = ReadonlyDeep<{ + type: typeof SHOW_PIN_CHANGE_MODAL; +}>; + // Not to be confused with pinned messages type TogglePinReminderActionType = ReadonlyDeep<{ type: typeof TOGGLE_PIN_REMINDER; @@ -592,6 +610,7 @@ export type GlobalModalsActionType = ReadonlyDeep< | HideKeyTransparencyErrorDialogActionType | HideKeyTransparencyOnboardingDialogActionType | HideLowDiskSpaceBackupImportModalActionType + | HidePinChangeModalActionType | HideSendAnywayDialogActiontype | HideStoriesSettingsActionType | HideTapToViewNotAvailableModalActionType @@ -611,6 +630,7 @@ export type GlobalModalsActionType = ReadonlyDeep< | ShowKeyTransparencyOnboardingDialogActionType | ShowLowDiskSpaceBackupImportModalActionType | ShowMediaPermissionsModalActionType + | ShowPinChangeModalActionType | ShowSendAnywayDialogActionType | ShowShortcutGuideModalActionType | ShowStickerPackPreviewActionType @@ -663,6 +683,7 @@ export const actions = { hideKeyTransparencyErrorDialog, hideKeyTransparencyOnboardingDialog, hideLowDiskSpaceBackupImportModal, + hidePinChangeModal, hideStoriesSettings, hideTapToViewNotAvailableModal, hideTerminateGroupFailedModal, @@ -681,6 +702,7 @@ export const actions = { showKeyTransparencyErrorDialog, showKeyTransparencyOnboardingDialog, showLowDiskSpaceBackupImportModal, + showPinChangeModal, showShareCallLinkViaSignal, showShortcutGuideModal, showStickerPackPreview, @@ -689,6 +711,7 @@ export const actions = { showTerminateGroupFailedModal, showUserNotFoundModal, showWhatsNewModal, + submitPinChangeModal, toggleAboutContactModal, toggleAddUserToAnotherGroupModal, toggleCallLinkAddNameModal, @@ -1198,6 +1221,47 @@ function toggleConfirmationModal( }; } +function hidePinChangeModal(): HidePinChangeModalActionType { + return { + type: HIDE_PIN_CHANGE_MODAL, + }; +} + +function showPinChangeModal(): ShowPinChangeModalActionType { + return { + type: SHOW_PIN_CHANGE_MODAL, + }; +} + +function submitPinChangeModal( + pin: string +): ThunkAction< + void, + RootStateType, + unknown, + HidePinChangeModalActionType | ToastActionType +> { + return async dispatch => { + try { + await itemStorage.put('svrPin', pin); + await registrationJobQueue.add({ + type: 'StoreSVR', + id: generateUuid(), + reason: 'submitPinChangeModal', + }); + drop(pinReminderService.resetPinReminderTimes()); + + dispatch(showToast({ toastType: ToastType.PinChangeCompleted })); + } catch (error) { + log.error(`submitPinChangeModal: error changing PIN`, toLogFormat(error)); + } + + dispatch({ + type: HIDE_PIN_CHANGE_MODAL, + }); + }; +} + function maybeShowPinReminder(): ThunkAction< void, RootStateType, @@ -1604,6 +1668,7 @@ export function getEmptyState(): GlobalModalsStateType { draftGifMessageSendModalProps: null, editNicknameAndNoteModalProps: null, errorModalProps: null, + isPinChangeModalVisible: false, isProfileNameWarningModalVisible: false, profileNameWarningModalConversationType: undefined, isShortcutGuideModalVisible: false, @@ -1858,6 +1923,20 @@ export function reducer( }; } + if (action.type === HIDE_PIN_CHANGE_MODAL) { + return { + ...state, + isPinChangeModalVisible: false, + }; + } + + if (action.type === SHOW_PIN_CHANGE_MODAL) { + return { + ...state, + isPinChangeModalVisible: true, + }; + } + if (action.type === TOGGLE_PIN_REMINDER) { return { ...state, diff --git a/ts/state/smart/GlobalModalContainer.preload.tsx b/ts/state/smart/GlobalModalContainer.preload.tsx index 2fe82c77cf..f07d25f605 100644 --- a/ts/state/smart/GlobalModalContainer.preload.tsx +++ b/ts/state/smart/GlobalModalContainer.preload.tsx @@ -47,6 +47,7 @@ import { SmartPinMessageDialog } from './PinMessageDialog.preload.tsx'; import { SmartGroupMemberLabelInfoModal } from './GroupMemberLabelInfoModal.preload.tsx'; import { SmartTerminateGroupFailedModal } from './TerminateGroupFailedModal.preload.tsx'; import { SmartPinReminderModal } from './PinReminderModal.preload.tsx'; +import { SmartPinChangeModal } from './PinChangeModal.preload.tsx'; function renderCallLinkAddNameModal(): JSX.Element { return ; @@ -124,6 +125,10 @@ function renderPinMessageDialog(): JSX.Element { return ; } +function renderPinChangeModal(): JSX.Element { + return ; +} + function renderPinReminderModal(): JSX.Element { return ; } @@ -192,6 +197,7 @@ export const SmartGlobalModalContainer = memo( notePreviewModalProps, pinMessageDialogData, pinReminderState, + isPinChangeModalVisible, isProfileNameWarningModalVisible, profileNameWarningModalConversationType, isShortcutGuideModalVisible, @@ -333,6 +339,7 @@ export const SmartGlobalModalContainer = memo( isAboutContactModalVisible={aboutContactModalState != null} isKeyTransparencyErrorVisible={isKeyTransparencyErrorVisible} isKeyTransparencyOnboardingVisible={isKeyTransparencyOnboardingVisible} + isPinChangeModalVisible={isPinChangeModalVisible} isProfileNameWarningModalVisible={isProfileNameWarningModalVisible} isShortcutGuideModalVisible={isShortcutGuideModalVisible} isSignalConnectionsVisible={isSignalConnectionsVisible} @@ -361,6 +368,7 @@ export const SmartGlobalModalContainer = memo( renderMessageRequestActionsConfirmation } renderNotePreviewModal={renderNotePreviewModal} + renderPinChangeModal={renderPinChangeModal} renderPinMessageDialog={renderPinMessageDialog} renderPinReminderModal={renderPinReminderModal} renderPlaintextExportWorkflow={renderPlaintextExportWorkflow} diff --git a/ts/state/smart/PinChangeModal.preload.tsx b/ts/state/smart/PinChangeModal.preload.tsx new file mode 100644 index 0000000000..4a414c67be --- /dev/null +++ b/ts/state/smart/PinChangeModal.preload.tsx @@ -0,0 +1,21 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +import { memo } from 'react'; +import { useSelector } from 'react-redux'; + +import { getIntl } from '../selectors/user.std.ts'; +import { useGlobalModalActions } from '../ducks/globalModals.preload.ts'; +import { PinChangeModal } from '../../components/PinChangeModal.dom.tsx'; + +export const SmartPinChangeModal = memo(function SmartPinChangeModal() { + const i18n = useSelector(getIntl); + const { hidePinChangeModal, submitPinChangeModal } = useGlobalModalActions(); + + return ( + + ); +}); diff --git a/ts/state/smart/PinReminderModal.preload.tsx b/ts/state/smart/PinReminderModal.preload.tsx index c5baf3cca4..581201c6c3 100644 --- a/ts/state/smart/PinReminderModal.preload.tsx +++ b/ts/state/smart/PinReminderModal.preload.tsx @@ -13,7 +13,7 @@ import { PinReminderModal } from '../../components/PinReminderModal.dom.tsx'; export const SmartPinReminderModal = memo(function SmartPinReminderModal() { const i18n = useSelector(getIntl); const pinReminderState = useSelector(getPinReminderModalProps); - const { togglePinReminder } = useGlobalModalActions(); + const { showPinChangeModal, togglePinReminder } = useGlobalModalActions(); const handleCancel = useCallback(() => { togglePinReminder(PinReminderState.Megaphone); @@ -35,6 +35,7 @@ export const SmartPinReminderModal = memo(function SmartPinReminderModal() { i18n={i18n} open onCancel={handleCancel} + onForgotPin={showPinChangeModal} onPinEntry={handlePinEntry} /> ); diff --git a/ts/state/smart/Preferences.preload.tsx b/ts/state/smart/Preferences.preload.tsx index 100d8ed579..9fc5ef9389 100644 --- a/ts/state/smart/Preferences.preload.tsx +++ b/ts/state/smart/Preferences.preload.tsx @@ -129,6 +129,7 @@ import { } from '../../types/StorageKeys.std.ts'; import type { BlockedConversation } from '../../components/Preferences.dom.tsx'; import { pinReminderService } from '../../services/pinReminder.preload.ts'; +import { useGlobalModalActions } from '../ducks/globalModals.preload.ts'; function renderUpdateDialog( props: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }> @@ -240,6 +241,7 @@ export function SmartPreferences(): JSX.Element | null { const { internalAddDonationReceipt } = useDonationsActions(); const { startPlaintextExport, startLocalBackupExport } = useBackupActions(); const { addVisibleMegaphone } = useMegaphonesActions(); + const { showPinChangeModal } = useGlobalModalActions(); // Selectors @@ -1200,6 +1202,7 @@ export function SmartPreferences(): JSX.Element | null { setGlobalDefaultConversationColor={setGlobalDefaultConversationColor} setSettingsLocation={setSettingsLocation} shouldShowUpdateDialog={shouldShowUpdateDialog} + showPinChangeModal={showPinChangeModal} showToast={showToast} startLocalBackupExport={startLocalBackupExport} startPlaintextExport={startPlaintextExport} diff --git a/ts/types/Toast.dom.tsx b/ts/types/Toast.dom.tsx index 515424fb45..8ebdd8cad0 100644 --- a/ts/types/Toast.dom.tsx +++ b/ts/types/Toast.dom.tsx @@ -73,6 +73,7 @@ export enum ToastType { OriginalMessageNotFound = 'OriginalMessageNotFound', PinnedConversationsFull = 'PinnedConversationsFull', PinnedMessageNotFound = 'PinnedMessageNotFound', + PinChangeCompleted = 'PinChangeCompleted', PinReminderCompleted = 'PinReminderCompleted', PollNotFound = 'PollNotFound', ReactionFailed = 'ReactionFailed', @@ -224,6 +225,7 @@ export type AnyToast = maxPinnedConversations: number; } | { toastType: ToastType.PinnedMessageNotFound } + | { toastType: ToastType.PinChangeCompleted } | { toastType: ToastType.PinReminderCompleted } | { toastType: ToastType.PollNotFound } | { toastType: ToastType.ReactionFailed }