mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-02-15 07:28:59 +00:00
Settings Tab: Ensure that navigation to it is handled elsewhere
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -1361,12 +1361,42 @@ export async function startApp(): Promise<void> {
|
||||
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', () => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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<EditMode | undefined>();
|
||||
|
||||
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(() => {
|
||||
|
||||
@@ -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<PageType>(Page.SendStory);
|
||||
|
||||
const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n);
|
||||
const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard({
|
||||
i18n,
|
||||
name: 'SendStoryModal',
|
||||
});
|
||||
|
||||
const [selectedListIds, setSelectedListIds] = useState<
|
||||
Set<StoryDistributionIdString>
|
||||
@@ -944,13 +947,17 @@ export function SendStoryModal({
|
||||
);
|
||||
}
|
||||
|
||||
const onTryClose = useCallback(() => {
|
||||
confirmDiscardIf(selectedContacts.length > 0, onClose);
|
||||
}, [confirmDiscardIf, selectedContacts, onClose]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!confirmDiscardModal && (
|
||||
<PagedModal
|
||||
modalName="SendStoryModal"
|
||||
theme={theme === ThemeType.dark ? Theme.Dark : Theme.Light}
|
||||
onClose={() => confirmDiscardIf(selectedContacts.length > 0, onClose)}
|
||||
onClose={onTryClose}
|
||||
>
|
||||
{modal}
|
||||
</PagedModal>
|
||||
|
||||
@@ -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<string | undefined>(
|
||||
undefined
|
||||
@@ -284,6 +295,10 @@ export function StoriesSettingsModal({
|
||||
const [selectedContacts, setSelectedContacts] = useState<
|
||||
Array<ConversationType>
|
||||
>([]);
|
||||
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({
|
||||
<PagedModal
|
||||
modalName="StoriesSettingsModal"
|
||||
moduleClassName="StoriesSettingsModal"
|
||||
onClose={() =>
|
||||
confirmDiscardIf(selectedContacts.length > 0, hideStoriesSettings)
|
||||
}
|
||||
onClose={onTryClose}
|
||||
>
|
||||
{modal}
|
||||
</PagedModal>
|
||||
|
||||
@@ -254,6 +254,7 @@ export function StoryCreator({
|
||||
imageSrc={attachmentUrl}
|
||||
imageToBlurHash={imageToBlurHash}
|
||||
installedPacks={installedPacks}
|
||||
isCreatingStory
|
||||
isFormattingEnabled={isFormattingEnabled}
|
||||
isSending={isSending}
|
||||
onClose={onClose}
|
||||
|
||||
@@ -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 = (
|
||||
<div className="StoryViewsNRepliesModal__not-a-member">
|
||||
@@ -484,6 +496,10 @@ export function StoryViewsNRepliesModal({
|
||||
return null;
|
||||
}
|
||||
|
||||
if (confirmDiscardModal) {
|
||||
return confirmDiscardModal;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
@@ -493,7 +509,7 @@ export function StoryViewsNRepliesModal({
|
||||
StoryViewsNRepliesModal: true,
|
||||
'StoryViewsNRepliesModal--group': Boolean(group),
|
||||
})}
|
||||
onClose={onClose}
|
||||
onClose={onTryClose}
|
||||
padded={false}
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
|
||||
@@ -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({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{showConfirmDiscardModal && (
|
||||
<ConfirmDiscardDialog
|
||||
i18n={i18n}
|
||||
onClose={() => setShowConfirmDiscardModal(false)}
|
||||
onDiscard={onClose}
|
||||
/>
|
||||
)}
|
||||
{confirmDiscardModal}
|
||||
</div>
|
||||
</FocusScope>
|
||||
);
|
||||
|
||||
@@ -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<HTMLFormElement> = event => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -228,12 +256,16 @@ export function EditConversationAttributesModal({
|
||||
</>
|
||||
);
|
||||
|
||||
if (confirmDiscardModal) {
|
||||
return confirmDiscardModal;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
modalName="EditConversationAttributesModal"
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
onClose={onTryClose}
|
||||
title={i18n('icu:updateGroupAttributes__title')}
|
||||
modalFooter={modalFooter}
|
||||
>
|
||||
|
||||
@@ -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<Omit<PropsType, 'i18n'> | null>(null);
|
||||
const confirmElement = props ? (
|
||||
<ConfirmDiscardDialog i18n={i18n} {...props} />
|
||||
) : null;
|
||||
const confirmDiscardPromise = useRef<
|
||||
ExplodePromiseResultType<BeforeNavigateResponse> | 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<BeforeNavigateResponse>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
103
ts/services/BeforeNavigate.ts
Normal file
103
ts/services/BeforeNavigate.ts
Normal file
@@ -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<BeforeNavigateResponse>;
|
||||
export type BeforeNavigateEntry = {
|
||||
name: string;
|
||||
callback: BeforeNavigateCallback;
|
||||
};
|
||||
|
||||
export class BeforeNavigateService {
|
||||
#beforeNavigateCallbacks = new Set<BeforeNavigateEntry>();
|
||||
|
||||
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<boolean> {
|
||||
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<BeforeNavigateResponse> {
|
||||
await sleep(ms);
|
||||
return BeforeNavigateResponse.TimedOut;
|
||||
}
|
||||
|
||||
export const beforeNavigateService = new BeforeNavigateService();
|
||||
@@ -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<AddressableAttachmentType>
|
||||
@@ -467,6 +468,7 @@ export const setup = (options: {
|
||||
|
||||
const Services = {
|
||||
backups: backupsService,
|
||||
beforeNavigate: beforeNavigateService,
|
||||
calling,
|
||||
initializeGroupCredentialFetcher,
|
||||
initializeNetworkObserver,
|
||||
|
||||
@@ -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",
|
||||
|
||||
6
ts/window.d.ts
vendored
6
ts/window.d.ts
vendored
@@ -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<void>;
|
||||
initializeNetworkObserver: (
|
||||
network: ReduxActions['network'],
|
||||
getAuthSocketStatus: () => SocketStatus
|
||||
) => void;
|
||||
initializeUpdateListener: (updates: ReduxActions['updates']) => void;
|
||||
retryPlaceholders?: RetryPlaceholders;
|
||||
lightSessionResetQueue?: PQueue;
|
||||
retryPlaceholders?: RetryPlaceholders;
|
||||
storage: typeof StorageService;
|
||||
};
|
||||
SettingsWindowProps?: SettingsWindowPropsType;
|
||||
|
||||
Reference in New Issue
Block a user