Fix click handling for stories

This commit is contained in:
Fedor Indutny 2022-09-27 13:24:21 -07:00 committed by GitHub
parent c0082adc56
commit 698c7a7739
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 274 additions and 46 deletions

View File

@ -578,6 +578,7 @@ export async function startApp(): Promise<void> {
try {
await new Promise<void>((resolve, reject) => {
window.showConfirmationDialog({
dialogName: 'deleteOldIndexedDBData',
onTopOfEverything: true,
cancelText: window.i18n('quit'),
confirmStyle: 'negative',

View File

@ -142,6 +142,7 @@ export const AddUserToAnotherGroupModal = ({
<>
{!selectedGroup && (
<Modal
modalName="AddUserToAnotherGroupModal"
hasXButton
i18n={i18n}
onClose={toggleAddUserToAnotherGroupModal}
@ -190,6 +191,7 @@ export const AddUserToAnotherGroupModal = ({
{selectedGroupId && selectedGroup && (
<ConfirmationDialog
dialogName="AddUserToAnotherGroupModal__confirm"
title={i18n('AddUserToAnotherGroupModal__confirm-title')}
i18n={i18n}
onClose={() => setSelectedGroupId(undefined)}

View File

@ -21,7 +21,7 @@ export const Alert: FunctionComponent<PropsType> = ({
onClose,
title,
}) => (
<Modal i18n={i18n} onClose={onClose} title={title}>
<Modal modalName="Alert" i18n={i18n} onClose={onClose} title={title}>
{body}
<Modal.ButtonFooter>
<Button onClick={onClose}>{i18n('Confirmation--confirm')}</Button>

View File

@ -27,6 +27,7 @@ export const AnnouncementsOnlyGroupBanner = ({
<>
{isShowingAdmins && (
<Modal
modalName="AnnouncmentsOnlyGroupBanner"
i18n={i18n}
onClose={() => setIsShowingAdmins(false)}
title={i18n('AnnouncementsOnlyGroupBanner--modal')}

View File

@ -93,6 +93,7 @@ function BadgeDialogWithBadges({
return (
<Modal
modalName="BadgeDialog"
hasXButton
moduleClassName="BadgeDialog"
i18n={i18n}

View File

@ -12,6 +12,7 @@ export function BadgeSustainerInstructionsDialog({
}: Readonly<{ i18n: LocalizerType; onClose: () => unknown }>): ReactElement {
return (
<Modal
modalName="BadgeSustainerInstructionsDialog"
hasXButton
moduleClassName="BadgeSustainerInstructionsDialog"
i18n={i18n}

View File

@ -135,7 +135,12 @@ export const CallingDeviceSelection = ({
: undefined;
return (
<Modal i18n={i18n} theme={Theme.Dark} onClose={toggleSettings}>
<Modal
modalName="CallingDeviceSelection"
i18n={i18n}
theme={Theme.Dark}
onClose={toggleSettings}
>
<div className="module-calling-device-selection">
<button
type="button"

View File

@ -84,6 +84,7 @@ export const CallingSelectPresentingSourcesModal = ({
return (
<Modal
modalName="CallingSelectPresentingSourcesModal"
hasXButton
i18n={i18n}
moduleClassName="module-CallingSelectPresentingSourcesModal"

View File

@ -36,6 +36,7 @@ export function CaptchaDialog(props: Readonly<PropsType>): JSX.Element {
if (isClosing && !isPending) {
return (
<Modal
modalName="CaptchaDialog"
moduleClassName="module-Modal"
i18n={i18n}
title={i18n('CaptchaDialog--can-close__title')}
@ -72,6 +73,7 @@ export function CaptchaDialog(props: Readonly<PropsType>): JSX.Element {
return (
<Modal
modalName="CaptchaDialog.pending"
moduleClassName="module-Modal--important"
i18n={i18n}
title={i18n('CaptchaDialog__title')}

View File

@ -125,6 +125,7 @@ export const ChatColorPicker = ({
{customColorToEdit ? renderCustomColorEditorWrapper() : null}
{confirmResetWhat ? (
<ConfirmationDialog
dialogName="ChatColorPicker.confirmReset"
actions={[
{
action: resetDefaultChatColor,
@ -151,6 +152,7 @@ export const ChatColorPicker = ({
) : null}
{confirmResetAll ? (
<ConfirmationDialog
dialogName="ChatColorPicker.confirmResetAll"
actions={[
{
action: resetAllChatColors,
@ -332,6 +334,7 @@ const CustomColorBubble = ({
<>
{confirmDeleteCount ? (
<ConfirmationDialog
dialogName="ChatColorPicker.confirmDelete"
actions={[
{
action: onDelete,
@ -433,6 +436,7 @@ const CustomColorEditorWrapper = ({
return (
<Modal
modalName="ChatColorPicker"
hasXButton
i18n={i18n}
moduleClassName="ChatColorPicker__modal"

View File

@ -18,6 +18,7 @@ export const ConfirmDiscardDialog = ({
}: PropsType): JSX.Element | null => {
return (
<ConfirmationDialog
dialogName="ConfirmDiscardDialog"
actions={[
{
action: onDiscard,

View File

@ -18,6 +18,7 @@ export default {
export const _ConfirmationDialog = (): JSX.Element => {
return (
<ConfirmationDialog
dialogName="test"
i18n={i18n}
onClose={action('onClose')}
title={text('Title', 'Foo bar banana baz?')}
@ -46,6 +47,7 @@ _ConfirmationDialog.story = {
export const CustomCancelText = (): JSX.Element => {
return (
<ConfirmationDialog
dialogName="test"
cancelText="Nah"
i18n={i18n}
onClose={action('onClose')}

View File

@ -19,6 +19,7 @@ export type ActionSpec = {
export type OwnProps = Readonly<{
actions?: Array<ActionSpec>;
dialogName: string;
cancelButtonVariant?: ButtonVariant;
cancelText?: string;
children?: React.ReactNode;
@ -58,6 +59,7 @@ function getButtonVariant(
export const ConfirmationDialog = React.memo(
({
actions = [],
dialogName,
cancelButtonVariant,
cancelText,
children,
@ -94,8 +96,11 @@ export const ConfirmationDialog = React.memo(
const hasActions = Boolean(actions.length);
const modalName = `ConfirmationDialog.${dialogName}`;
return (
<ModalHost
modalName={modalName}
noMouseClose={noMouseClose}
onClose={close}
onTopOfEverything={onTopOfEverything}
@ -104,6 +109,7 @@ export const ConfirmationDialog = React.memo(
>
<animated.div style={modalStyles}>
<ModalWindow
modalName={modalName}
hasXButton={hasXButton}
i18n={i18n}
moduleClassName={moduleClassName}

View File

@ -111,7 +111,10 @@ export function ContextMenu<T>({
closeCurrentOpenContextMenu = undefined;
return true;
},
{ containerElements: [referenceElement, popperElement] }
{
containerElements: [referenceElement, popperElement],
name: 'ContextMenu',
}
);
}, [isMenuShowing, referenceElement, popperElement]);

View File

@ -35,6 +35,7 @@ export function CrashReportDialog(props: Readonly<PropsType>): JSX.Element {
return (
<Modal
modalName="CrashReportDialog"
moduleClassName="module-Modal--important"
i18n={i18n}
title={i18n('CrashReportDialog__title')}

View File

@ -83,7 +83,10 @@ export function CustomizingPreferredReactionsModal({
deselectDraftEmoji();
return true;
},
{ containerElements: [popperElement] }
{
containerElements: [popperElement],
name: 'CustomizingPreferredReactionsModal.draftEmoji',
}
);
}, [isSomethingSelected, popperElement, deselectDraftEmoji]);
@ -103,6 +106,7 @@ export function CustomizingPreferredReactionsModal({
return (
<Modal
modalName="CustomizingPreferredReactionsModal"
moduleClassName="module-CustomizingPreferredReactionsModal"
hasXButton
i18n={i18n}

View File

@ -72,6 +72,7 @@ export function DisappearingTimeDialog(props: PropsType): JSX.Element {
return (
<ConfirmationDialog
dialogName="DisappearingTimerDialog"
moduleClassName={CSS_MODULE}
i18n={i18n}
theme={theme}

View File

@ -27,6 +27,7 @@ export const ErrorModal = (props: PropsType): JSX.Element => {
return (
<Modal
modalName="ErrorModal"
i18n={i18n}
onClose={onClose}
title={title || i18n('ErrorModal--title')}

View File

@ -280,6 +280,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
<>
{cannotMessage && (
<ConfirmationDialog
dialogName="ForwardMessageModal.confirm"
cancelText={i18n('Confirmation--confirm')}
i18n={i18n}
onClose={() => setCannotMessage(false)}
@ -288,6 +289,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
</ConfirmationDialog>
)}
<ModalHost
modalName="ForwardMessageModal"
onEscape={handleBackOrClose}
onClose={close}
overlayStyles={overlayStyles}

View File

@ -118,6 +118,7 @@ export const GlobalModalContainer = ({
return (
<ConfirmationDialog
dialogName="GlobalModalContainer.userNotFound"
cancelText={i18n('ok')}
cancelButtonVariant={ButtonVariant.Secondary}
i18n={i18n}

View File

@ -244,6 +244,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
<>
{showBlockInfo && (
<ConfirmationDialog
dialogName="GroupCallRemoteParticipant.blockInfo"
cancelText={i18n('ok')}
i18n={i18n}
onClose={() => {

View File

@ -54,7 +54,7 @@ export function GroupDialog(props: Readonly<PropsType>): JSX.Element {
}
return (
<ModalHost onClose={onClose}>
<ModalHost modalName="GroupDialog" onClose={onClose}>
<div className="module-GroupDialog">
<button
aria-label={i18n('close')}

View File

@ -69,7 +69,10 @@ export class MainHeader extends React.Component<PropsType, StateType> {
return true;
},
{ containerElements: [popperRoot, this.containerRef] }
{
containerElements: [popperRoot, this.containerRef],
name: 'MainHeader.showAvatarPopup',
}
);
this.setState({

View File

@ -87,7 +87,10 @@ export const MediaQualitySelector = ({
handleClose();
return true;
},
{ containerElements: [popperRoot, buttonRef] }
{
containerElements: [popperRoot, buttonRef],
name: 'MediaQualitySelector',
}
);
}, [menuShowing, popperRoot, handleClose]);

View File

@ -23,7 +23,7 @@ const LOREM_IPSUM =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.';
export const BareBonesShort = (): JSX.Element => (
<Modal i18n={i18n} useFocusTrap={false}>
<Modal modalName="test" i18n={i18n} useFocusTrap={false}>
Hello world!
</Modal>
);
@ -33,7 +33,7 @@ BareBonesShort.story = {
};
export const BareBonesLong = (): JSX.Element => (
<Modal i18n={i18n} useFocusTrap={false}>
<Modal modalName="test" i18n={i18n} useFocusTrap={false}>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
@ -46,7 +46,7 @@ BareBonesLong.story = {
};
export const BareBonesLongWithButton = (): JSX.Element => (
<Modal i18n={i18n}>
<Modal modalName="test" i18n={i18n}>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
@ -62,7 +62,13 @@ BareBonesLongWithButton.story = {
};
export const TitleXButtonBodyAndButtonFooter = (): JSX.Element => (
<Modal i18n={i18n} title="Hello world" onClose={onClose} hasXButton>
<Modal
modalName="test"
i18n={i18n}
title="Hello world"
onClose={onClose}
hasXButton
>
{LOREM_IPSUM}
<Modal.ButtonFooter>
<Button onClick={noop}>Okay</Button>
@ -75,7 +81,7 @@ TitleXButtonBodyAndButtonFooter.story = {
};
export const LotsOfButtonsInTheFooter = (): JSX.Element => (
<Modal i18n={i18n} onClose={onClose}>
<Modal modalName="test" i18n={i18n} onClose={onClose}>
Hello world!
<Modal.ButtonFooter>
<Button onClick={noop}>Okay</Button>
@ -98,7 +104,13 @@ LotsOfButtonsInTheFooter.story = {
};
export const LongBodyWithTitle = (): JSX.Element => (
<Modal i18n={i18n} title="Hello world" onClose={onClose} useFocusTrap={false}>
<Modal
modalName="test"
i18n={i18n}
title="Hello world"
onClose={onClose}
useFocusTrap={false}
>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
@ -111,7 +123,7 @@ LongBodyWithTitle.story = {
};
export const LongBodyWithTitleAndButton = (): JSX.Element => (
<Modal i18n={i18n} title="Hello world" onClose={onClose}>
<Modal modalName="test" i18n={i18n} title="Hello world" onClose={onClose}>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
@ -128,6 +140,7 @@ LongBodyWithTitleAndButton.story = {
export const LongBodyWithLongTitleAndXButton = (): JSX.Element => (
<Modal
modalName="test"
i18n={i18n}
title={LOREM_IPSUM.slice(0, 104)}
hasXButton
@ -145,7 +158,13 @@ LongBodyWithLongTitleAndXButton.story = {
};
export const WithStickyButtonsLongBody = (): JSX.Element => (
<Modal hasStickyButtons hasXButton i18n={i18n} onClose={onClose}>
<Modal
modalName="test"
hasStickyButtons
hasXButton
i18n={i18n}
onClose={onClose}
>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
@ -162,7 +181,13 @@ WithStickyButtonsLongBody.story = {
};
export const WithStickyButtonsShortBody = (): JSX.Element => (
<Modal hasStickyButtons hasXButton i18n={i18n} onClose={onClose}>
<Modal
modalName="test"
hasStickyButtons
hasXButton
i18n={i18n}
onClose={onClose}
>
<p>{LOREM_IPSUM.slice(0, 140)}</p>
<Modal.ButtonFooter>
<Button onClick={noop}>Okay</Button>
@ -176,7 +201,13 @@ WithStickyButtonsShortBody.story = {
};
export const StickyFooterLotsOfButtons = (): JSX.Element => (
<Modal hasStickyButtons i18n={i18n} onClose={onClose} title="OK">
<Modal
modalName="test"
hasStickyButtons
i18n={i18n}
onClose={onClose}
title="OK"
>
<p>{LOREM_IPSUM}</p>
<Modal.ButtonFooter>
<Button onClick={noop}>Okay</Button>
@ -200,6 +231,7 @@ StickyFooterLotsOfButtons.story = {
export const WithBackButton = (): JSX.Element => (
<Modal
modalName="test"
hasXButton
i18n={i18n}
onBackButtonClick={noop}

View File

@ -19,6 +19,7 @@ import { useRefMerger } from '../hooks/useRefMerger';
type PropsType = {
children: ReactNode;
modalName: string;
hasStickyButtons?: boolean;
hasXButton?: boolean;
i18n: LocalizerType;
@ -39,6 +40,7 @@ const BASE_CLASS_NAME = 'module-Modal';
export function Modal({
children,
modalName,
hasStickyButtons,
hasXButton,
i18n,
@ -61,6 +63,7 @@ export function Modal({
return (
<ModalHost
modalName={modalName}
moduleClassName={moduleClassName}
noMouseClose={noMouseClose}
onClose={close}
@ -70,6 +73,7 @@ export function Modal({
>
<animated.div style={modalStyles}>
<ModalWindow
modalName={modalName}
hasStickyButtons={hasStickyButtons}
hasXButton={hasXButton}
i18n={i18n}

View File

@ -11,13 +11,17 @@ import { noop } from 'lodash';
import type { ModalConfigType } from '../hooks/useAnimated';
import type { Theme } from '../util/theme';
import { assertDev } from '../util/assert';
import { getClassNamesFor } from '../util/getClassNamesFor';
import { themeClassName } from '../util/theme';
import { useEscapeHandling } from '../hooks/useEscapeHandling';
import { usePrevious } from '../hooks/usePrevious';
import { handleOutsideClick } from '../util/handleOutsideClick';
import * as log from '../logging/log';
export type PropsType = Readonly<{
children: React.ReactElement;
modalName: string;
moduleClassName?: string;
noMouseClose?: boolean;
onClose: () => unknown;
@ -31,6 +35,7 @@ export type PropsType = Readonly<{
export const ModalHost = React.memo(
({
children,
modalName,
moduleClassName,
noMouseClose,
onClose,
@ -42,6 +47,15 @@ export const ModalHost = React.memo(
}: PropsType) => {
const [root, setRoot] = React.useState<HTMLElement | null>(null);
const containerRef = React.useRef<HTMLDivElement | null>(null);
const previousModalName = usePrevious(modalName, modalName);
if (previousModalName !== modalName) {
log.error(
`ModalHost detected conflict between ${previousModalName} ` +
`and ${modalName}. Consider using "key" attributes on both modals.`
);
assertDev(false, 'Modal conflict');
}
useEffect(() => {
const div = document.createElement('div');
@ -64,9 +78,9 @@ export const ModalHost = React.memo(
onClose();
return true;
},
{ containerElements: [containerRef] }
{ containerElements: [containerRef], name: modalName }
);
}, [noMouseClose, onClose]);
}, [noMouseClose, onClose, containerRef, modalName]);
const className = classNames([
theme ? themeClassName(theme) : undefined,
@ -99,10 +113,14 @@ export const ModalHost = React.memo(
return false;
}
const titleBar = document.querySelector(
'.TitleBarContainer__title'
// TitleBar should always receive clicks. Quill suggestions
// are placed in the document.body so they should be exempt
// too.
const exemptParent = target.closest(
'.TitleBarContainer__title, ' +
'.module-composition-input__suggestions'
);
if (titleBar?.contains(target)) {
if (exemptParent) {
return true;
}
return false;

View File

@ -42,6 +42,7 @@ export const MyStories = ({
<>
{confirmDeleteStory && (
<ConfirmationDialog
dialogName="MyStories.delete"
actions={[
{
text: i18n('delete'),

View File

@ -26,6 +26,7 @@ export const NeedsScreenRecordingPermissionsModal = ({
}: PropsType): JSX.Element => {
return (
<Modal
modalName="NeedsScreenRecordingPermissionsModal"
i18n={i18n}
title={i18n('calling__presenting--permission-title')}
theme={Theme.Dark}

View File

@ -54,6 +54,7 @@ export const OutgoingGiftBadgeModal = ({
return (
<Modal
modalName="OutgoingGiftBadgeModal"
i18n={i18n}
moduleClassName={`${CLASS_NAME}__container`}
onClose={hideOutgoingGiftBadgeModal}

View File

@ -1021,6 +1021,7 @@ export const Preferences = ({
</SettingsRow>
{confirmDelete ? (
<ConfirmationDialog
dialogName="Preference.deleteAllData"
actions={[
{
action: doDeleteAllData,

View File

@ -822,6 +822,7 @@ export const ProfileEditor = ({
<>
{usernameEditState === UsernameEditState.ConfirmingDelete && (
<ConfirmationDialog
dialogName="ProfileEditor.confirmDeleteUsername"
i18n={i18n}
onClose={() => setUsernameEditState(UsernameEditState.Editing)}
actions={[
@ -837,6 +838,7 @@ export const ProfileEditor = ({
)}
{usernameEditState === UsernameEditState.ShowingErrorPopup && (
<ConfirmationDialog
dialogName="ProfileEditor.usernameError"
cancelText={i18n('ok')}
cancelButtonVariant={ButtonVariant.Secondary}
i18n={i18n}

View File

@ -47,6 +47,7 @@ export const ProfileEditorModal = ({
if (hasError) {
return (
<ConfirmationDialog
dialogName="ProfileEditorModal.error"
cancelText={i18n('Confirmation--confirm')}
i18n={i18n}
onClose={toggleProfileEditorHasError}
@ -59,6 +60,7 @@ export const ProfileEditorModal = ({
return (
<>
<Modal
modalName="ProfileEditorModal"
hasStickyButtons
hasXButton
i18n={i18n}

View File

@ -65,7 +65,12 @@ export const SafetyNumberChangeDialog = ({
if (selectedContact) {
return (
<Modal hasXButton i18n={i18n} onClose={onClose}>
<Modal
modalName="SafetyNumberChangeDialog"
hasXButton
i18n={i18n}
onClose={onClose}
>
{renderSafetyNumber({ contactID: selectedContact.id, onClose })}
</Modal>
);
@ -73,6 +78,7 @@ export const SafetyNumberChangeDialog = ({
return (
<ConfirmationDialog
dialogName="SafetyNumberChangeDialog.confirmSend"
actions={[
{
action: onConfirm,

View File

@ -17,6 +17,7 @@ export const SafetyNumberModal = ({
}: PropsType): JSX.Element | null => {
return (
<Modal
modalName="SafetyNumberModal"
hasXButton
i18n={i18n}
moduleClassName="module-SafetyNumberViewer__modal"

View File

@ -753,6 +753,7 @@ export const SendStoryModal = ({
return (
<>
<Modal
modalName="SendStoryModal"
hasStickyButtons
hasXButton
i18n={i18n}
@ -803,6 +804,7 @@ export const SendStoryModal = ({
</Modal>
{confirmRemoveGroupId && (
<ConfirmationDialog
dialogName="SendStoryModal.confirmRemoveGroupId"
actions={[
{
action: () => {
@ -823,6 +825,7 @@ export const SendStoryModal = ({
)}
{confirmDeleteListId && (
<ConfirmationDialog
dialogName="SendStoryModal.confirmDeleteList"
actions={[
{
action: () => {

View File

@ -18,7 +18,12 @@ export const SignalConnectionsModal = ({
onClose,
}: PropsType): JSX.Element => {
return (
<Modal hasXButton i18n={i18n} onClose={onClose}>
<Modal
modalName="SignalConnectionsModal"
hasXButton
i18n={i18n}
onClose={onClose}
>
<div className="SignalConnectionsModal">
<i className="SignalConnectionsModal__icon" />

View File

@ -273,6 +273,7 @@ export const StoriesSettingsModal = ({
return (
<>
<Modal
modalName="StoriesSettingsModal"
hasStickyButtons={hasStickyButtons}
hasXButton
i18n={i18n}
@ -300,6 +301,7 @@ export const StoriesSettingsModal = ({
</Modal>
{confirmDeleteListId && (
<ConfirmationDialog
dialogName="StoriesSettings.deleteList"
actions={[
{
action: () => {
@ -496,6 +498,7 @@ export const DistributionListSettings = ({
{confirmRemoveMember && (
<ConfirmationDialog
dialogName="StoriesSettings.confirmRemoveMember"
actions={[
{
action: () =>

View File

@ -196,6 +196,7 @@ export const StoryDetailsModal = ({
return (
<Modal
modalName="StoryDetailsModal"
hasXButton
i18n={i18n}
moduleClassName="StoryDetailsModal"

View File

@ -190,6 +190,7 @@ export const StoryListItem = ({
</ContextMenu>
{hasConfirmHideStory && (
<ConfirmationDialog
dialogName="StoryListItem.hideStory"
actions={[
{
action: () => onHideStory(conversationId),

View File

@ -824,6 +824,7 @@ export const StoryViewer = ({
)}
{hasConfirmHideStory && (
<ConfirmationDialog
dialogName="StoryViewer.confirmHideStory"
actions={[
{
action: () => {
@ -844,6 +845,7 @@ export const StoryViewer = ({
)}
{confirmDeleteStory && (
<ConfirmationDialog
dialogName="StoryViewer.deleteStory"
actions={[
{
text: i18n('delete'),

View File

@ -149,9 +149,12 @@ export const StoryViewsNRepliesModal = ({
const insertEmoji = useCallback(
(e: EmojiPickDataType) => {
onUseEmoji(e);
if (inputApiRef.current) {
inputApiRef.current.insertEmoji(e);
onUseEmoji(e);
}
},
[onUseEmoji]
[inputApiRef, onUseEmoji]
);
const [referenceElement, setReferenceElement] =
@ -203,7 +206,7 @@ export const StoryViewsNRepliesModal = ({
onEditorStateChange={messageText => {
setMessageBodyText(messageText);
}}
onPickEmoji={insertEmoji}
onPickEmoji={onUseEmoji}
onSubmit={(...args) => {
inputApiRef.current?.reset();
shouldScrollToBottomRef.current = true;
@ -446,6 +449,7 @@ export const StoryViewsNRepliesModal = ({
return (
<Modal
modalName="StoryViewsNRepliesModal"
i18n={i18n}
moduleClassName="StoryViewsNRepliesModal"
onClose={onClose}

View File

@ -278,7 +278,10 @@ export const TextStoryCreator = ({
setIsColorPickerShowing(false);
return true;
},
{ containerElements: [colorPickerPopperRef, colorPickerPopperButtonRef] }
{
containerElements: [colorPickerPopperRef, colorPickerPopperButtonRef],
name: 'TextStoryCreator.colorPicker',
}
);
}, [isColorPickerShowing, colorPickerPopperRef, colorPickerPopperButtonRef]);

View File

@ -78,6 +78,7 @@ export const WhatsNewModal = ({
return (
<Modal
modalName="WhatsNewModal"
hasXButton
i18n={i18n}
onClose={hideWhatsNewModal}

View File

@ -168,6 +168,7 @@ export const AudioCapture = ({
confirmationDialog = (
<ConfirmationDialog
dialogName="AudioCapture.sendAnyway"
i18n={i18n}
onCancel={clickCancel}
onClose={noop}
@ -188,6 +189,7 @@ export const AudioCapture = ({
) {
confirmationDialog = (
<ConfirmationDialog
dialogName="AudioCapture.error"
i18n={i18n}
onCancel={clickCancel}
onClose={noop}

View File

@ -25,7 +25,12 @@ export function ChatSessionRefreshedDialog(
const [focusRef] = useRestoreFocus();
return (
<Modal hasXButton={false} onClose={onClose} i18n={i18n}>
<Modal
modalName="ChatSessionRefreshedDialog"
hasXButton={false}
onClose={onClose}
i18n={i18n}
>
<div className="module-chat-session-refreshed-dialog">
<div className="module-chat-session-refreshed-dialog__image">
<img

View File

@ -112,6 +112,7 @@ export const ContactModal = ({
modalNode = (
<ConfirmationDialog
dialogName="ContactModal.toggleAdmin"
actions={[
{
action: () => toggleAdmin(conversation.id, contact.id),
@ -166,6 +167,7 @@ export const ContactModal = ({
return (
<Modal
modalName="ContactModal"
moduleClassName="ContactModal__modal"
hasXButton
i18n={i18n}

View File

@ -349,6 +349,7 @@ export const ContactSpoofingReviewDialog: FunctionComponent<
return (
<Modal
modalName="ContactSpoofingReviewDialog"
hasXButton
i18n={i18n}
moduleClassName="module-ContactSpoofingReviewDialog"

View File

@ -225,6 +225,7 @@ export const ConversationHero = ({
</div>
{isShowingMessageRequestWarning && (
<ConfirmationDialog
dialogName="ConversationHere.messageRequestWarning"
i18n={i18n}
onClose={closeMessageRequestWarning}
actions={[

View File

@ -32,7 +32,12 @@ export function DeliveryIssueDialog(props: PropsType): React.ReactElement {
const [focusRef] = useRestoreFocus();
return (
<Modal hasXButton={false} onClose={onClose} i18n={i18n}>
<Modal
modalName="DeliveryIssueDialog"
hasXButton={false}
onClose={onClose}
i18n={i18n}
>
<section>
<div className="module-delivery-issue-dialog__image">
<img

View File

@ -40,6 +40,7 @@ export const GroupDescription = ({
<>
{showFullDescription && (
<Modal
modalName="GroupDescription"
hasXButton
i18n={i18n}
onClose={() => setShowFullDescription(false)}

View File

@ -176,6 +176,7 @@ function GroupV2Detail({
modalNode = (
<Modal
modalName="GroupV2Change.ViewingGroupDescription"
hasXButton
i18n={i18n}
title={groupName}
@ -200,6 +201,7 @@ function GroupV2Detail({
modalNode = (
<ConfirmationDialog
dialogName="GroupV2Change.confirmBlockLinkRequests"
title={i18n('PendingRequests--block--title')}
actions={[
{

View File

@ -2422,7 +2422,10 @@ export class Message extends React.PureComponent<Props, State> {
this.toggleReactionViewer(true);
return true;
},
{ containerElements: [root, this.reactionsContainerRef] }
{
containerElements: [root, this.reactionsContainerRef],
name: 'Message.reactionViewer',
}
);
return {
@ -2458,7 +2461,7 @@ export class Message extends React.PureComponent<Props, State> {
this.toggleReactionPicker(true);
return true;
},
{ containerElements: [root] }
{ containerElements: [root], name: 'Message.reactionPicker' }
);
return {

View File

@ -41,6 +41,7 @@ export const MessageRequestActionsConfirmation = ({
if (state === MessageRequestState.blocking) {
return (
<ConfirmationDialog
dialogName="messageRequestActionsConfirmation.blocking"
i18n={i18n}
onClose={() => {
onChangeState(MessageRequestState.default);
@ -77,6 +78,7 @@ export const MessageRequestActionsConfirmation = ({
if (state === MessageRequestState.unblocking) {
return (
<ConfirmationDialog
dialogName="messageRequestActionsConfirmation.unblocking"
i18n={i18n}
onClose={() => {
onChangeState(MessageRequestState.default);
@ -104,6 +106,7 @@ export const MessageRequestActionsConfirmation = ({
if (state === MessageRequestState.deleting) {
return (
<ConfirmationDialog
dialogName="messageRequestActionsConfirmation.deleting"
i18n={i18n}
onClose={() => {
onChangeState(MessageRequestState.default);

View File

@ -31,6 +31,7 @@ export const RemoveGroupMemberConfirmationDialog: FunctionComponent<
return (
<ConfirmationDialog
dialogName="RemoveGroupMemberConfirmationDialog"
actions={[
{
action: onRemove,

View File

@ -269,7 +269,10 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
};
return (
<ModalHost onClose={onClose}>
<ModalHost
modalName="AddGroupMembersModal.ChooseGroupMembersModal"
onClose={onClose}
>
<div className="module-AddGroupMembersModal module-AddGroupMembersModal--choose-members">
<button
aria-label={i18n('close')}

View File

@ -79,7 +79,10 @@ export const ConfirmAdditionsModal: FunctionComponent<PropsType> = ({
}
return (
<ModalHost onClose={onClose}>
<ModalHost
modalName="AddGroupMemberModal.ConfirmAdditionsModal"
onClose={onClose}
>
<div className="module-AddGroupMembersModal module-AddGroupMembersModal--confirm-adds">
<h1 className="module-AddGroupMembersModal__header">{headerText}</h1>
{requestState === RequestState.InactiveWithError && (

View File

@ -290,6 +290,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
case ModalState.UnmuteNotifications:
modalNode = (
<ConfirmationDialog
dialogName="ConversationDetails.unmuteNotifications"
actions={[
{
action: () => setMuteExpiration(0),

View File

@ -167,6 +167,7 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
</PanelSection>
{confirmLeave && (
<ConfirmationDialog
dialogName="ConversationDetailsAction.confirmLeave"
actions={[
{
text: i18n(
@ -186,6 +187,7 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
{confirmGroupBlock && (
<ConfirmationDialog
dialogName="ConversationDetailsAction.confirmBlock"
actions={[
{
text: i18n(
@ -206,6 +208,7 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
)}
{confirmGroupUnblock && (
<ConfirmationDialog
dialogName="ConversationDetailsAction.confirmUnblock"
actions={[
{
text: i18n(
@ -227,6 +230,7 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
{confirmDirectBlock && (
<ConfirmationDialog
dialogName="ConversationDetailsAction.confirmDirectBlock"
actions={[
{
text: i18n('MessageRequests--block'),
@ -245,6 +249,7 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
)}
{confirmDirectUnblock && (
<ConfirmationDialog
dialogName="ConversationDetailsAction.confirmDirectUnblock"
actions={[
{
text: i18n('MessageRequests--unblock'),

View File

@ -46,6 +46,7 @@ export const ConversationNotificationsModal = ({
return (
<Modal
modalName="ConversationNotificationsModal"
hasStickyButtons
hasXButton
onClose={onClose}

View File

@ -227,6 +227,7 @@ export const EditConversationAttributesModal: FunctionComponent<PropsType> = ({
return (
<Modal
modalName="EditConversationAttributesModal"
hasStickyButtons
hasXButton
i18n={i18n}

View File

@ -76,6 +76,7 @@ export const GroupLinkManagement: React.ComponentType<PropsType> = ({
<>
{hasGenerateNewLinkDialog && (
<ConfirmationDialog
dialogName="GroupLinkManagement.resetLink"
actions={[
{
action: () => {

View File

@ -215,6 +215,7 @@ function MembershipActionConfirmation({
return (
<ConfirmationDialog
dialogName="PendingInvites.actionConfirmation"
actions={[
{
action: modalAction,

View File

@ -75,6 +75,7 @@ export const PhoneNumberCheckbox: FunctionComponent<PropsType> = React.memo(
if (isModalVisible) {
modal = (
<ConfirmationDialog
dialogName="PhoneNumberCheckbox.invalidPhoneNumber"
cancelText={i18n('ok')}
cancelButtonVariant={ButtonVariant.Secondary}
i18n={i18n}

View File

@ -72,6 +72,7 @@ export const StartNewConversation: FunctionComponent<Props> = React.memo(
if (isModalVisible) {
modal = (
<ConfirmationDialog
dialogName="StartNewConversation.invalidPhoneNumber"
cancelText={i18n('ok')}
cancelButtonVariant={ButtonVariant.Secondary}
i18n={i18n}

View File

@ -109,7 +109,7 @@ export const EmojiButton = React.memo(
handleClose();
return true;
},
{ containerElements: [popperRoot, buttonRef] }
{ containerElements: [popperRoot, buttonRef], name: 'EmojiButton' }
);
}, [open, handleClose, popperRoot]);

View File

@ -151,6 +151,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
>
{this.isEditingAvatar && (
<Modal
modalName="LeftPaneSetGroupMetadataHelper.AvatarEditor"
hasStickyButtons
hasXButton
i18n={i18n}

View File

@ -173,7 +173,10 @@ export const StickerButton = React.memo(
setOpen(false);
return true;
},
{ containerElements: [popperRoot, buttonRef] }
{
containerElements: [popperRoot, buttonRef],
name: 'StickerButton',
}
);
}, [open, popperRoot, setOpen]);

View File

@ -92,6 +92,7 @@ export const StickerManagerPackRow = React.memo(
<>
{uninstalling ? (
<ConfirmationDialog
dialogName="StickerManagerPackRow.confirmUninstall"
i18n={i18n}
onClose={clearUninstalling}
actions={[

View File

@ -188,6 +188,7 @@ export const StickerPreviewModal = React.memo((props: Props) => {
>
{confirmingUninstall ? (
<ConfirmationDialog
dialogName="StickerPreviewModal.confirmUninstall"
i18n={i18n}
onClose={onClose}
actions={[

View File

@ -1,7 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useState } from 'react';
import { useState, useCallback } from 'react';
import type { SpringValues } from '@react-spring/web';
import { useChain, useSpring, useSpringRef } from '@react-spring/web';
@ -59,9 +59,10 @@ export function useAnimated(
});
useChain(isOpen ? [overlayRef, modalRef] : [modalRef, overlayRef]);
const close = useCallback(() => setIsOpen(false), []);
return {
close: () => setIsOpen(false),
close,
overlayStyles,
modalStyles,
};

View File

@ -20,6 +20,7 @@ import {
import { Emoji } from '../../components/emoji/Emoji';
import type { EmojiPickDataType } from '../../components/emoji/EmojiPicker';
import { getBlotTextPartitions, matchBlotTextPartitions } from '../util';
import { handleOutsideClick } from '../../util/handleOutsideClick';
import * as log from '../../logging/log';
const Keyboard = Quill.import('modules/keyboard');
@ -41,6 +42,8 @@ export class EmojiCompletion {
quill: Quill;
outsideClickDestructor: () => void;
constructor(quill: Quill, options: EmojiPickerOptions) {
this.results = [];
this.index = 0;
@ -48,6 +51,18 @@ export class EmojiCompletion {
this.root = document.body.appendChild(document.createElement('div'));
this.quill = quill;
// Just to make sure that we don't propagate outside clicks until this
// is closed.
this.outsideClickDestructor = handleOutsideClick(
() => {
return true;
},
{
name: 'quill.emoji.completion',
containerElements: [this.root],
}
);
const clearResults = () => {
if (this.results.length) {
this.reset();
@ -93,6 +108,7 @@ export class EmojiCompletion {
}
destroy(): void {
this.outsideClickDestructor();
this.root.remove();
}

View File

@ -16,6 +16,7 @@ import type { LocalizerType, ThemeType } from '../../types/Util';
import type { MemberRepository } from '../memberRepository';
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges';
import { matchBlotTextPartitions } from '../util';
import { handleOutsideClick } from '../../util/handleOutsideClick';
import { sameWidthModifier } from '../../util/popperUtil';
export type MentionCompletionOptions = {
@ -42,6 +43,8 @@ export class MentionCompletion {
suggestionListRef: RefObject<HTMLDivElement>;
outsideClickDestructor: () => void;
constructor(quill: Quill, options: MentionCompletionOptions) {
this.results = [];
this.index = 0;
@ -50,6 +53,18 @@ export class MentionCompletion {
this.quill = quill;
this.suggestionListRef = React.createRef<HTMLDivElement>();
// Just to make sure that we don't propagate outside clicks until this
// is closed.
this.outsideClickDestructor = handleOutsideClick(
() => {
return true;
},
{
name: 'quill.emoji.completion',
containerElements: [this.root],
}
);
const clearResults = () => {
if (this.results.length) {
this.clearResults();
@ -77,6 +92,7 @@ export class MentionCompletion {
}
destroy(): void {
this.outsideClickDestructor();
this.root.remove();
}

View File

@ -10,6 +10,7 @@
type ConfirmationDialogViewProps = {
onTopOfEverything?: boolean;
dialogName: string;
cancelText?: string;
confirmStyle?: 'affirmative' | 'negative';
message: string;
@ -51,6 +52,7 @@ function showConfirmationDialog(options: ConfirmationDialogViewProps) {
window.ReactDOM.render(
// eslint-disable-next-line react/react-in-jsx-scope, react/jsx-no-undef
<window.Signal.Components.ConfirmationDialog
dialogName={options.dialogName}
onTopOfEverything={options.onTopOfEverything}
actions={[
{

View File

@ -18,7 +18,7 @@ export const createGroupV2JoinModal = (
return (
<Provider store={store}>
<ModalHost onClose={onClose}>
<ModalHost modalName="createGroupV2JoinModal" onClose={onClose}>
<SmartGroupV2JoinDialog {...props} />
</ModalHost>
</Provider>

View File

@ -19,6 +19,7 @@ function runFakeClickHandlers(event: MouseEvent): void {
}
export type HandleOutsideClickOptionsType = Readonly<{
name: string;
containerElements: ReadonlyArray<ContainerElementType>;
}>;
@ -38,16 +39,14 @@ export const handleOutsideClick = (
}
return elem.current?.contains(target);
});
// Clicked inside of one of container elements - stop processing
if (isInside) {
return false;
return true;
}
const isHandled = handler(target);
if (!isHandled) {
return false;
}
return true;
// Stop processing if requested by handler function
return handler(target);
};
fakeClickHandlers.push(handleEvent);

View File

@ -589,6 +589,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
onStartGroupMigration: () => this.startMigrationToGV2(),
onCancelJoinRequest: async () => {
await window.showConfirmationDialog({
dialogName: 'GroupV2CancelRequestToJoin',
message: window.i18n(
'GroupV2--join--cancel-request-to-join--confirmation'
),
@ -1689,6 +1690,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}
window.showConfirmationDialog({
dialogName: 'deleteMessage',
confirmStyle: 'negative',
message: window.i18n('deleteWarning'),
okText: window.i18n('delete'),
@ -1713,6 +1715,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}
window.showConfirmationDialog({
dialogName: 'deleteMessageForEveryone',
confirmStyle: 'negative',
message: window.i18n('deleteForEveryoneWarning'),
okText: window.i18n('delete'),
@ -2324,6 +2327,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
const { model }: { model: ConversationModel } = this;
window.showConfirmationDialog({
dialogName: 'destroyMessages',
confirmStyle: 'negative',
message: window.i18n('deleteConversationConfirmation'),
okText: window.i18n('delete'),

1
ts/window.d.ts vendored
View File

@ -103,6 +103,7 @@ export { Long } from 'long';
// Synced with the type in ts/shims/showConfirmationDialog
// we are duplicating it here because that file cannot import/export.
type ConfirmationDialogViewProps = {
dialogName: string;
cancelText?: string;
confirmStyle?: 'affirmative' | 'negative';
message: string;