From ffb2f3cb7e0f3a088a29e697beeb71abe31aade1 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Wed, 21 May 2025 09:03:31 +1000 Subject: [PATCH] Settings Tab: Ensure that navigation to it is handled elsewhere --- .storybook/preview.tsx | 11 ++ ts/background.ts | 38 ++++++- ts/components/CallScreen.tsx | 18 +++ ts/components/CompositionArea.tsx | 1 + ts/components/MediaEditor.tsx | 14 ++- ts/components/SendStoryModal.tsx | 13 ++- ts/components/StoriesSettingsModal.tsx | 23 +++- ts/components/StoryCreator.tsx | 1 + ts/components/StoryViewsNRepliesModal.tsx | 18 ++- ts/components/TextStoryCreator.tsx | 25 ++--- .../EditConversationAttributesModal.tsx | 36 +++++- ts/hooks/useConfirmDiscard.tsx | 77 +++++++++++-- ts/services/BeforeNavigate.ts | 103 ++++++++++++++++++ ts/signal.ts | 2 + ts/util/lint/exceptions.json | 48 ++++++++ ts/window.d.ts | 6 +- 16 files changed, 391 insertions(+), 43 deletions(-) create mode 100644 ts/services/BeforeNavigate.ts diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 33d117aa3d..3c1cf89d03 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -152,6 +152,17 @@ window.ConversationController = window.ConversationController || {}; window.ConversationController.isSignalConversationId = () => false; window.ConversationController.onConvoMessageMount = noop; window.reduxStore = mockStore; +window.Signal = { + Services: { + beforeNavigate: { + registerCallback: () => undefined, + unregisterCallback: () => undefined, + shouldCancelNavigation: () => { + throw new Error('Not implemented'); + }, + }, + }, +}; function withStrictMode(Story, context) { return ( diff --git a/ts/background.ts b/ts/background.ts index 67f0ef8c17..ddb0394684 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -1361,12 +1361,42 @@ export async function startApp(): Promise { window.reduxActions.app.openStandalone(); }); - window.Whisper.events.on('stageLocalBackupForImport', () => { - drop(backupsService._internalStageLocalBackupForImport()); + let openingSettingsTab = false; + window.Whisper.events.on('openSettingsTab', async () => { + const logId = 'openSettingsTab'; + try { + if (openingSettingsTab) { + log.info( + `${logId}: Already attempting to open settings tab, returning early` + ); + return; + } + + openingSettingsTab = true; + + const newTab = NavTab.Settings; + const needToCancel = + await window.Signal.Services.beforeNavigate.shouldCancelNavigation({ + context: logId, + newTab, + }); + + if (needToCancel) { + log.info(`${logId}: Cancelling navigation to the settings tab`); + return; + } + + window.reduxActions.nav.changeNavTab(newTab); + } finally { + if (!openingSettingsTab) { + log.warn(`${logId}: openingSettingsTab was already false in finally!`); + } + openingSettingsTab = false; + } }); - window.Whisper.events.on('openSettingsTab', () => { - window.reduxActions.nav.changeNavTab(NavTab.Settings); + window.Whisper.events.on('stageLocalBackupForImport', () => { + drop(backupsService._internalStageLocalBackupForImport()); }); window.Whisper.events.on('powerMonitorSuspend', () => { diff --git a/ts/components/CallScreen.tsx b/ts/components/CallScreen.tsx index 317ef2234e..148470f642 100644 --- a/ts/components/CallScreen.tsx +++ b/ts/components/CallScreen.tsx @@ -97,6 +97,7 @@ import { isEmojiVariantValue, } from './fun/data/emojis'; import { useFunEmojiLocalizer } from './fun/useFunEmojiLocalizer'; +import { BeforeNavigateResponse } from '../services/BeforeNavigate'; export type PropsType = { activeCall: ActiveCallType; @@ -314,6 +315,23 @@ export function CallScreen({ }, 5000); return clearTimeout.bind(null, timer); }, [showControls, showReactionPicker, stickyControls, controlsHover]); + useEffect(() => { + const name = 'CallScreen'; + const callback = async () => { + togglePip(); + return BeforeNavigateResponse.MadeChanges; + }; + window.Signal.Services.beforeNavigate.registerCallback({ + callback, + name, + }); + return () => { + window.Signal.Services.beforeNavigate.unregisterCallback({ + callback, + name, + }); + }; + }, [togglePip]); const [selfViewHover, setSelfViewHover] = useState(false); const onSelfViewMouseEnter = useCallback(() => { diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index 88251cfb8f..958041ba54 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -1075,6 +1075,7 @@ export const CompositionArea = memo(function CompositionArea({ imageSrc={attachmentToEdit.url} imageToBlurHash={imageToBlurHash} installedPacks={installedPacks} + isCreatingStory={false} isFormattingEnabled={isFormattingEnabled} isSending={false} onClose={() => setAttachmentToEdit(undefined)} diff --git a/ts/components/MediaEditor.tsx b/ts/components/MediaEditor.tsx index aaca06287c..a9519a7002 100644 --- a/ts/components/MediaEditor.tsx +++ b/ts/components/MediaEditor.tsx @@ -78,6 +78,7 @@ export type MediaEditorResultType = Readonly<{ }>; export type PropsType = { + isCreatingStory: boolean; doneButtonLabel?: string; i18n: LocalizerType; imageSrc: string; @@ -152,6 +153,7 @@ export function MediaEditor({ doneButtonLabel, i18n, imageSrc, + isCreatingStory, isSending, onClose, onDone, @@ -370,11 +372,17 @@ export function MediaEditor({ const [editMode, setEditMode] = useState(); - const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n); + const tryClose = useRef<() => void | undefined>(); + const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard({ + i18n, + name: 'MediaEditor', + tryClose, + }); const onTryClose = useCallback(() => { - confirmDiscardIf(canUndo, onClose); - }, [confirmDiscardIf, canUndo, onClose]); + confirmDiscardIf(canUndo || isCreatingStory, onClose); + }, [confirmDiscardIf, canUndo, isCreatingStory, onClose]); + tryClose.current = onTryClose; // Keyboard support useEffect(() => { diff --git a/ts/components/SendStoryModal.tsx b/ts/components/SendStoryModal.tsx index 0cd8af8bce..d035d808f1 100644 --- a/ts/components/SendStoryModal.tsx +++ b/ts/components/SendStoryModal.tsx @@ -1,7 +1,7 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { noop, sortBy } from 'lodash'; import { SearchInput } from './SearchInput'; @@ -151,7 +151,10 @@ export function SendStoryModal({ }: PropsType): JSX.Element { const [page, setPage] = useState(Page.SendStory); - const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n); + const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard({ + i18n, + name: 'SendStoryModal', + }); const [selectedListIds, setSelectedListIds] = useState< Set @@ -944,13 +947,17 @@ export function SendStoryModal({ ); } + const onTryClose = useCallback(() => { + confirmDiscardIf(selectedContacts.length > 0, onClose); + }, [confirmDiscardIf, selectedContacts, onClose]); + return ( <> {!confirmDiscardModal && ( confirmDiscardIf(selectedContacts.length > 0, onClose)} + onClose={onTryClose} > {modal} diff --git a/ts/components/StoriesSettingsModal.tsx b/ts/components/StoriesSettingsModal.tsx index e09369ee6a..8159f5c6d9 100644 --- a/ts/components/StoriesSettingsModal.tsx +++ b/ts/components/StoriesSettingsModal.tsx @@ -2,7 +2,13 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { ReactNode } from 'react'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { noop } from 'lodash'; import type { ConversationType } from '../state/ducks/conversations'; @@ -261,7 +267,12 @@ export function StoriesSettingsModal({ setStoriesDisabled, getConversationByServiceId, }: PropsType): JSX.Element { - const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n); + const tryClose = useRef<() => void | undefined>(); + const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard({ + i18n, + name: 'StoriesSettingsModal', + tryClose, + }); const [listToEditId, setListToEditId] = useState( undefined @@ -284,6 +295,10 @@ export function StoriesSettingsModal({ const [selectedContacts, setSelectedContacts] = useState< Array >([]); + const onTryClose = useCallback(() => { + confirmDiscardIf(selectedContacts.length > 0, hideStoriesSettings); + }, [confirmDiscardIf, selectedContacts, hideStoriesSettings]); + tryClose.current = onTryClose; const resetChooseViewersScreen = useCallback(() => { setSelectedContacts([]); @@ -482,9 +497,7 @@ export function StoriesSettingsModal({ - confirmDiscardIf(selectedContacts.length > 0, hideStoriesSettings) - } + onClose={onTryClose} > {modal} diff --git a/ts/components/StoryCreator.tsx b/ts/components/StoryCreator.tsx index f60423121d..1d5babfb93 100644 --- a/ts/components/StoryCreator.tsx +++ b/ts/components/StoryCreator.tsx @@ -254,6 +254,7 @@ export function StoryCreator({ imageSrc={attachmentUrl} imageToBlurHash={imageToBlurHash} installedPacks={installedPacks} + isCreatingStory isFormattingEnabled={isFormattingEnabled} isSending={isSending} onClose={onClose} diff --git a/ts/components/StoryViewsNRepliesModal.tsx b/ts/components/StoryViewsNRepliesModal.tsx index 05c0e7bd73..1cc6ed1b57 100644 --- a/ts/components/StoryViewsNRepliesModal.tsx +++ b/ts/components/StoryViewsNRepliesModal.tsx @@ -41,6 +41,7 @@ import { isFunPickerEnabled } from './fun/isFunPickerEnabled'; import { FunEmojiPicker } from './fun/FunEmojiPicker'; import { FunEmojiPickerButton } from './fun/FunButton'; import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis'; +import { useConfirmDiscard } from '../hooks/useConfirmDiscard'; // Menu is disabled so these actions are inaccessible. We also don't support // link previews, tap to view messages, attachments, or gifts. Just regular @@ -241,6 +242,17 @@ export function StoryViewsNRepliesModal({ } }, [currentTab, replies.length]); + const tryClose = useRef<() => void | undefined>(); + const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard({ + i18n, + name: 'StoryViewsNRepliesModal', + tryClose, + }); + const onTryClose = useCallback(() => { + confirmDiscardIf(emojiPickerOpen || messageBodyText.length > 0, onClose); + }, [confirmDiscardIf, emojiPickerOpen, messageBodyText, onClose]); + tryClose.current = onTryClose; + if (group && group.left) { composerElement = (
@@ -484,6 +496,10 @@ export function StoryViewsNRepliesModal({ return null; } + if (confirmDiscardModal) { + return confirmDiscardModal; + } + return ( <> diff --git a/ts/components/TextStoryCreator.tsx b/ts/components/TextStoryCreator.tsx index b7b42f7096..bfc5d4cb31 100644 --- a/ts/components/TextStoryCreator.tsx +++ b/ts/components/TextStoryCreator.tsx @@ -29,13 +29,13 @@ import { import { convertShortName } from './emoji/lib'; import { objectMap } from '../util/objectMap'; import { handleOutsideClick } from '../util/handleOutsideClick'; -import { ConfirmDiscardDialog } from './ConfirmDiscardDialog'; import { Spinner } from './Spinner'; import { FunEmojiPicker } from './fun/FunEmojiPicker'; import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis'; import { getEmojiVariantByKey } from './fun/data/emojis'; import { FunEmojiPickerButton } from './fun/FunButton'; import { isFunPickerEnabled } from './fun/isFunPickerEnabled'; +import { useConfirmDiscard } from '../hooks/useConfirmDiscard'; export type PropsType = { debouncedMaybeGrabLinkPreview: ( @@ -148,11 +148,16 @@ export function TextStoryCreator({ recentEmojis, emojiSkinToneDefault, }: PropsType): JSX.Element { - const [showConfirmDiscardModal, setShowConfirmDiscardModal] = useState(false); - + const tryClose = useRef<() => void | undefined>(); + const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard({ + i18n, + name: 'SendStoryModal', + tryClose, + }); const onTryClose = useCallback(() => { - setShowConfirmDiscardModal(true); - }, [setShowConfirmDiscardModal]); + confirmDiscardIf(true, onClose); + }, [confirmDiscardIf, onClose]); + tryClose.current = onTryClose; const [isEditingText, setIsEditingText] = useState(false); const [selectedBackground, setSelectedBackground] = @@ -290,8 +295,6 @@ export function TextStoryCreator({ isEditingText, isLinkPreviewInputShowing, colorPickerPopperButtonRef, - showConfirmDiscardModal, - setShowConfirmDiscardModal, onTryClose, ]); @@ -653,13 +656,7 @@ export function TextStoryCreator({
- {showConfirmDiscardModal && ( - setShowConfirmDiscardModal(false)} - onDiscard={onClose} - /> - )} + {confirmDiscardModal} ); diff --git a/ts/components/conversation/conversation-details/EditConversationAttributesModal.tsx b/ts/components/conversation/conversation-details/EditConversationAttributesModal.tsx index 6b26fe469b..a5fd79ec12 100644 --- a/ts/components/conversation/conversation-details/EditConversationAttributesModal.tsx +++ b/ts/components/conversation/conversation-details/EditConversationAttributesModal.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { FormEventHandler } from 'react'; -import React, { useRef, useState } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import type { LocalizerType } from '../../../types/Util'; import { Modal } from '../../Modal'; @@ -20,6 +20,7 @@ import type { SaveAvatarToDiskActionType, } from '../../../types/Avatar'; import type { AvatarColorType } from '../../../types/Colors'; +import { useConfirmDiscard } from '../../../hooks/useConfirmDiscard'; type PropsType = { avatarColor?: AvatarColorType; @@ -79,6 +80,13 @@ export function EditConversationAttributesModal({ const trimmedTitle = rawTitle.trim(); const trimmedDescription = rawGroupDescription.trim(); + const tryClose = useRef<() => void | undefined>(); + const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard({ + i18n, + name: 'EditConversationAttributesModal', + tryClose, + }); + const focusRef = (el: null | HTMLElement) => { if (el) { el.focus(); @@ -103,6 +111,26 @@ export function EditConversationAttributesModal({ hasGroupDescriptionChanged) && trimmedTitle.length > 0; + const onTryClose = useCallback(() => { + confirmDiscardIf( + isRequestActive || + hasAvatarChanged || + hasChangedExternally || + hasGroupDescriptionChanged || + hasTitleChanged, + onClose + ); + }, [ + confirmDiscardIf, + isRequestActive, + hasAvatarChanged, + hasChangedExternally, + hasGroupDescriptionChanged, + hasTitleChanged, + onClose, + ]); + tryClose.current = onTryClose; + const onSubmit: FormEventHandler = event => { event.preventDefault(); @@ -228,12 +256,16 @@ export function EditConversationAttributesModal({ ); + if (confirmDiscardModal) { + return confirmDiscardModal; + } + return ( diff --git a/ts/hooks/useConfirmDiscard.tsx b/ts/hooks/useConfirmDiscard.tsx index 862eb75b29..058c7e5ef7 100644 --- a/ts/hooks/useConfirmDiscard.tsx +++ b/ts/hooks/useConfirmDiscard.tsx @@ -1,31 +1,90 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { useState } from 'react'; - -import type { PropsType } from '../components/ConfirmDiscardDialog'; +import React, { useEffect, useRef, useState } from 'react'; import { ConfirmDiscardDialog } from '../components/ConfirmDiscardDialog'; +import { BeforeNavigateResponse } from '../services/BeforeNavigate'; +import { + explodePromise, + type ExplodePromiseResultType, +} from '../util/explodePromise'; + +import type { PropsType } from '../components/ConfirmDiscardDialog'; import type { LocalizerType } from '../types/Util'; -export function useConfirmDiscard( - i18n: LocalizerType -): [JSX.Element | null, (condition: boolean, callback: () => void) => void] { +export function useConfirmDiscard({ + i18n, + name, + tryClose, +}: { + i18n: LocalizerType; + name: string; + tryClose?: React.MutableRefObject<(() => void) | undefined>; +}): [ + JSX.Element | null, + (condition: boolean, discardChanges: () => void, cancel?: () => void) => void, +] { const [props, setProps] = useState | null>(null); const confirmElement = props ? ( ) : null; + const confirmDiscardPromise = useRef< + ExplodePromiseResultType | undefined + >(); - function confirmDiscardIf(condition: boolean, callback: () => void) { + useEffect(() => { + if (!tryClose) { + return; + } + + const callback = async () => { + const close = tryClose.current; + if (!close) { + return BeforeNavigateResponse.Noop; + } + + confirmDiscardPromise.current = explodePromise(); + close(); + return confirmDiscardPromise.current.promise; + }; + window.Signal.Services.beforeNavigate.registerCallback({ + name, + callback, + }); + + return () => { + window.Signal.Services.beforeNavigate.unregisterCallback({ + name, + callback, + }); + }; + }, [name, tryClose, confirmDiscardPromise]); + + function confirmDiscardIf( + condition: boolean, + discardChanges: () => void, + cancel?: () => void + ) { if (condition) { setProps({ onClose() { + confirmDiscardPromise.current?.resolve( + BeforeNavigateResponse.CancelNavigation + ); setProps(null); + cancel?.(); + }, + onDiscard() { + confirmDiscardPromise.current?.resolve( + BeforeNavigateResponse.WaitedForUser + ); + discardChanges(); }, - onDiscard: callback, }); } else { - callback(); + confirmDiscardPromise.current?.resolve(BeforeNavigateResponse.Noop); + discardChanges(); } } diff --git a/ts/services/BeforeNavigate.ts b/ts/services/BeforeNavigate.ts new file mode 100644 index 0000000000..b06519709d --- /dev/null +++ b/ts/services/BeforeNavigate.ts @@ -0,0 +1,103 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import * as log from '../logging/log'; + +import type { NavTab } from '../state/ducks/nav'; +import { SECOND } from '../util/durations'; +import { sleep } from '../util/sleep'; + +export enum BeforeNavigateResponse { + Noop = 'Noop', + MadeChanges = 'MadeChanges', + WaitedForUser = 'WaitedForUser', + CancelNavigation = 'CancelNavigation', + TimedOut = 'TimedOut', +} +export type BeforeNavigateCallback = ( + newTab: NavTab +) => Promise; +export type BeforeNavigateEntry = { + name: string; + callback: BeforeNavigateCallback; +}; + +export class BeforeNavigateService { + #beforeNavigateCallbacks = new Set(); + + private findMatchingEntry( + entry: BeforeNavigateEntry + ): BeforeNavigateEntry | undefined { + const { callback } = entry; + return Array.from(this.#beforeNavigateCallbacks).find( + item => item.callback === callback + ); + } + + registerCallback(entry: BeforeNavigateEntry): void { + const logId = 'registerCallback'; + const existing = this.findMatchingEntry(entry); + + if (existing) { + log.warn( + `${logId}: Overwriting duplicate callback for entry ${entry.name}` + ); + this.#beforeNavigateCallbacks.delete(existing); + } + + this.#beforeNavigateCallbacks.add(entry); + } + unregisterCallback(entry: BeforeNavigateEntry): void { + const logId = 'unregisterCallback'; + const existing = this.findMatchingEntry(entry); + + if (!existing) { + log.warn( + `${logId}: Didn't find matching callback for entry ${entry.name}` + ); + return; + } + + this.#beforeNavigateCallbacks.delete(existing); + } + + async shouldCancelNavigation({ + context, + newTab, + }: { + context: string; + newTab: NavTab; + }): Promise { + const logId = `shouldCancelNavigation/${context}`; + const entries = Array.from(this.#beforeNavigateCallbacks); + + for (let i = 0, max = entries.length; i < max; i += 1) { + const entry = entries[i]; + // eslint-disable-next-line no-await-in-loop + const response = await Promise.race([ + entry.callback(newTab), + timeOutAfter(5 * SECOND), + ]); + if (response === BeforeNavigateResponse.Noop) { + continue; + } + + log.info(`${logId}: ${entry.name} returned result ${response}`); + if ( + response === BeforeNavigateResponse.CancelNavigation || + response === BeforeNavigateResponse.TimedOut + ) { + return true; + } + } + + return false; + } +} + +async function timeOutAfter(ms: number): Promise { + await sleep(ms); + return BeforeNavigateResponse.TimedOut; +} + +export const beforeNavigateService = new BeforeNavigateService(); diff --git a/ts/signal.ts b/ts/signal.ts index d420806f55..7ffb636cd2 100644 --- a/ts/signal.ts +++ b/ts/signal.ts @@ -54,6 +54,7 @@ import type { LinkPreviewWithHydratedData, } from './types/message/LinkPreviews'; import type { StickerType, StickerWithHydratedData } from './types/Stickers'; +import { beforeNavigateService } from './services/BeforeNavigate'; type EncryptedReader = ( attachment: Partial @@ -467,6 +468,7 @@ export const setup = (options: { const Services = { backups: backupsService, + beforeNavigate: beforeNavigateService, calling, initializeGroupCredentialFetcher, initializeNetworkObserver, diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 9ccea19f88..4fc499522c 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -1348,6 +1348,14 @@ "reasonCategory": "usageTrusted", "updated": "2023-09-11T20:19:18.681Z" }, + { + "rule": "React-useRef", + "path": "ts/components/MediaEditor.tsx", + "line": " const tryClose = useRef<() => void | undefined>();", + "reasonCategory": "usageTrusted", + "updated": "2025-05-19T22:29:15.758Z", + "reasonDetail": "Holding on to a close function" + }, { "rule": "React-useRef", "path": "ts/components/MediaQualitySelector.tsx", @@ -1456,6 +1464,14 @@ "reasonCategory": "usageTrusted", "updated": "2024-11-16T00:33:41.092Z" }, + { + "rule": "React-useRef", + "path": "ts/components/StoriesSettingsModal.tsx", + "line": " const tryClose = useRef<() => void | undefined>();", + "reasonCategory": "usageTrusted", + "updated": "2025-05-19T22:29:15.758Z", + "reasonDetail": "Holding on to a close function" + }, { "rule": "React-useRef", "path": "ts/components/StoryImage.tsx", @@ -1498,6 +1514,14 @@ "reasonCategory": "usageTrusted", "updated": "2022-10-05T18:51:56.411Z" }, + { + "rule": "React-useRef", + "path": "ts/components/StoryViewsNRepliesModal.tsx", + "line": " const tryClose = useRef<() => void | undefined>();", + "reasonCategory": "usageTrusted", + "updated": "2025-05-19T23:20:52.448Z", + "reasonDetail": "Holding on to a close function" + }, { "rule": "React-useRef", "path": "ts/components/TextAttachment.tsx", @@ -1526,6 +1550,14 @@ "reasonCategory": "usageTrusted", "updated": "2022-06-16T23:23:32.306Z" }, + { + "rule": "React-useRef", + "path": "ts/components/TextStoryCreator.tsx", + "line": " const tryClose = useRef<() => void | undefined>();", + "reasonCategory": "usageTrusted", + "updated": "2025-05-19T22:29:15.758Z", + "reasonDetail": "Holding on to a close function" + }, { "rule": "React-useRef", "path": "ts/components/Tooltip.tsx", @@ -1687,6 +1719,14 @@ "reasonCategory": "usageTrusted", "updated": "2021-07-30T16:57:33.618Z" }, + { + "rule": "React-useRef", + "path": "ts/components/conversation/conversation-details/EditConversationAttributesModal.tsx", + "line": " const tryClose = useRef<() => void | undefined>();", + "reasonCategory": "usageTrusted", + "updated": "2025-05-19T23:20:52.448Z", + "reasonDetail": "Holding on to a close function" + }, { "rule": "React-useRef", "path": "ts/components/conversation/media-gallery/MediaGallery.tsx", @@ -1911,6 +1951,14 @@ "updated": "2022-06-14T22:04:43.988Z", "reasonDetail": "Handling outside click" }, + { + "rule": "React-useRef", + "path": "ts/hooks/useConfirmDiscard.tsx", + "line": " const confirmDiscardPromise = useRef<", + "reasonCategory": "usageTrusted", + "updated": "2025-05-19T22:29:15.758Z", + "reasonDetail": "Holding on to a promise" + }, { "rule": "React-useRef", "path": "ts/hooks/useIntersectionObserver.ts", diff --git a/ts/window.d.ts b/ts/window.d.ts index 4c4ec6de2b..a5a1b22009 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -53,6 +53,7 @@ import type { PropsPreloadType as PreferencesPropsType } from './components/Pref import type { WindowsNotificationData } from './services/notifications'; import type { QueryStatsOptions } from './sql/main'; import type { SocketStatuses } from './textsecure/SocketManager'; +import type { BeforeNavigateService } from './services/BeforeNavigate'; export { Long } from 'long'; @@ -151,16 +152,17 @@ export type SignalCoreType = { RemoteConfig: typeof RemoteConfig; ScreenShareWindowProps?: ScreenShareWindowPropsType; Services: { - calling: CallingClass; backups: BackupsService; + beforeNavigate: BeforeNavigateService; + calling: CallingClass; initializeGroupCredentialFetcher: () => Promise; initializeNetworkObserver: ( network: ReduxActions['network'], getAuthSocketStatus: () => SocketStatus ) => void; initializeUpdateListener: (updates: ReduxActions['updates']) => void; - retryPlaceholders?: RetryPlaceholders; lightSessionResetQueue?: PQueue; + retryPlaceholders?: RetryPlaceholders; storage: typeof StorageService; }; SettingsWindowProps?: SettingsWindowPropsType;