diff --git a/src/components/editor-page/app-bar/help-button/help-button.tsx b/src/components/editor-page/app-bar/help-button/help-button.tsx index e72b92699..734f040ff 100644 --- a/src/components/editor-page/app-bar/help-button/help-button.tsx +++ b/src/components/editor-page/app-bar/help-button/help-button.tsx @@ -1,20 +1,20 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { Fragment, useCallback, useState } from 'react' +import React, { Fragment } from 'react' import { Button } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon' import { HelpModal } from './help-modal' import { cypressId } from '../../../../utils/cypress-attribute' +import { useBooleanState } from '../../../../hooks/common/use-boolean-state' export const HelpButton: React.FC = () => { const { t } = useTranslation() - const [show, setShow] = useState(false) - const onHide = useCallback(() => setShow(false), []) + const [modalVisibility, showModal, closeModal] = useBooleanState() return ( @@ -24,10 +24,10 @@ export const HelpButton: React.FC = () => { className='ml-2 text-secondary' size='sm' variant='outline-light' - onClick={() => setShow(true)}> + onClick={showModal}> - + ) } diff --git a/src/components/editor-page/editor-pane/max-length-warning/max-length-warning.tsx b/src/components/editor-page/editor-pane/max-length-warning/max-length-warning.tsx index a13cdf436..f3d4437b0 100644 --- a/src/components/editor-page/editor-pane/max-length-warning/max-length-warning.tsx +++ b/src/components/editor-page/editor-pane/max-length-warning/max-length-warning.tsx @@ -1,33 +1,33 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { useCallback, useEffect, useRef, useState } from 'react' +import React, { useEffect, useRef } from 'react' import { MaxLengthWarningModal } from './max-length-warning-modal' import { useNoteMarkdownContent } from '../../../../hooks/common/use-note-markdown-content' import { useApplicationState } from '../../../../hooks/common/use-application-state' +import { useBooleanState } from '../../../../hooks/common/use-boolean-state' /** * Watches the length of the document and shows a warning modal to the user if the document length exceeds the configured value. */ export const MaxLengthWarning: React.FC = () => { - const [showMaxLengthWarningModal, setShowMaxLengthWarningModal] = useState(false) + const [modalVisibility, showModal, closeModal] = useBooleanState() const maxLengthWarningAlreadyShown = useRef(false) const maxDocumentLength = useApplicationState((state) => state.config.maxDocumentLength) - const hideWarning = useCallback(() => setShowMaxLengthWarningModal(false), []) const markdownContent = useNoteMarkdownContent() useEffect(() => { if (markdownContent.length > maxDocumentLength && !maxLengthWarningAlreadyShown.current) { - setShowMaxLengthWarningModal(true) + showModal() maxLengthWarningAlreadyShown.current = true } if (markdownContent.length <= maxDocumentLength) { maxLengthWarningAlreadyShown.current = false } - }, [markdownContent, maxDocumentLength]) + }, [markdownContent, maxDocumentLength, showModal]) - return + return } diff --git a/src/components/editor-page/renderer-pane/communicator-image-lightbox.tsx b/src/components/editor-page/renderer-pane/communicator-image-lightbox.tsx index d0ec8a2e3..a0a082e26 100644 --- a/src/components/editor-page/renderer-pane/communicator-image-lightbox.tsx +++ b/src/components/editor-page/renderer-pane/communicator-image-lightbox.tsx @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -12,30 +12,27 @@ import type { } from '../../render-page/window-post-message-communicator/rendering-message' import { CommunicationMessageType } from '../../render-page/window-post-message-communicator/rendering-message' import { useEditorReceiveHandler } from '../../render-page/window-post-message-communicator/hooks/use-editor-receive-handler' +import { useBooleanState } from '../../../hooks/common/use-boolean-state' export const CommunicatorImageLightbox: React.FC = () => { const [lightboxDetails, setLightboxDetails] = useState(undefined) - const [show, setShow] = useState(false) + const [modalVisibility, showModal, closeModal] = useBooleanState() useEditorReceiveHandler( CommunicationMessageType.IMAGE_CLICKED, useCallback( (values: ImageClickedMessage) => { setLightboxDetails?.(values.details) - setShow(true) + showModal() }, - [setLightboxDetails] + [showModal] ) ) - const hideLightbox = useCallback(() => { - setShow(false) - }, []) - return ( > = ({ hide, className }) => { useTranslation() - const [showDialog, setShowDialog] = useState(false) const noteId = useApplicationState((state) => state.noteDetails.id) - const openDialog = useCallback(() => setShowDialog(true), []) - const closeDialog = useCallback(() => setShowDialog(false), []) + const [modalVisibility, showModal, closeModal] = useBooleanState() const deleteNoteAndCloseDialog = useCallback(() => { - deleteNote(noteId) - .catch(showErrorNotification('landing.history.error.deleteNote.text')) - .finally(() => setShowDialog(false)) - }, [noteId]) + deleteNote(noteId).catch(showErrorNotification('landing.history.error.deleteNote.text')).finally(closeModal) + }, [closeModal, noteId]) return ( @@ -41,12 +37,10 @@ export const DeleteNoteSidebarEntry: React.FC + onClick={showModal}> - - - + ) } diff --git a/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-entry.tsx b/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-entry.tsx index c42005cf5..5f55faa8c 100644 --- a/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-entry.tsx +++ b/src/components/editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-entry.tsx @@ -1,18 +1,19 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { Fragment, useState } from 'react' +import React, { Fragment } from 'react' import { Trans, useTranslation } from 'react-i18next' import { NoteInfoModal } from '../../document-bar/note-info/note-info-modal' import { SidebarButton } from '../sidebar-button/sidebar-button' import type { SpecificSidebarEntryProps } from '../types' import { cypressId } from '../../../../utils/cypress-attribute' +import { useBooleanState } from '../../../../hooks/common/use-boolean-state' export const NoteInfoSidebarEntry: React.FC = ({ className, hide }) => { - const [showModal, setShowModal] = useState(false) + const [modalVisibility, showModal, closeModal] = useBooleanState() useTranslation() return ( @@ -21,11 +22,11 @@ export const NoteInfoSidebarEntry: React.FC = ({ clas hide={hide} className={className} icon={'line-chart'} - onClick={() => setShowModal(true)} + onClick={showModal} {...cypressId('sidebar-btn-document-info')}> - setShowModal(false)} /> + ) } diff --git a/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry.tsx b/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry.tsx index e6cbd43d7..a1a86f7bc 100644 --- a/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry.tsx +++ b/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry.tsx @@ -1,25 +1,26 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { Fragment, useState } from 'react' +import React, { Fragment } from 'react' import { Trans, useTranslation } from 'react-i18next' import { PermissionModal } from '../../document-bar/permissions/permission-modal' import { SidebarButton } from '../sidebar-button/sidebar-button' import type { SpecificSidebarEntryProps } from '../types' +import { useBooleanState } from '../../../../hooks/common/use-boolean-state' export const PermissionsSidebarEntry: React.FC = ({ className, hide }) => { - const [showModal, setShowModal] = useState(false) + const [modalVisibility, showModal, closeModal] = useBooleanState() useTranslation() return ( - setShowModal(true)}> + - setShowModal(false)} /> + ) } diff --git a/src/components/editor-page/sidebar/specific-sidebar-entries/revision-sidebar-entry.tsx b/src/components/editor-page/sidebar/specific-sidebar-entries/revision-sidebar-entry.tsx index 2d0b9b732..8f134fec1 100644 --- a/src/components/editor-page/sidebar/specific-sidebar-entries/revision-sidebar-entry.tsx +++ b/src/components/editor-page/sidebar/specific-sidebar-entries/revision-sidebar-entry.tsx @@ -1,30 +1,25 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { Fragment, useCallback, useState } from 'react' +import React, { Fragment } from 'react' import { Trans } from 'react-i18next' import { RevisionModal } from '../../document-bar/revisions/revision-modal' import { SidebarButton } from '../sidebar-button/sidebar-button' import type { SpecificSidebarEntryProps } from '../types' +import { useBooleanState } from '../../../../hooks/common/use-boolean-state' export const RevisionSidebarEntry: React.FC = ({ className, hide }) => { - const [showModal, setShowModal] = useState(false) - const onHide = useCallback(() => { - setShowModal(false) - }, []) - const onShow = useCallback(() => { - setShowModal(true) - }, []) + const [modalVisibility, showModal, closeModal] = useBooleanState() return ( - + - + ) } diff --git a/src/components/editor-page/sidebar/specific-sidebar-entries/share-sidebar-entry.tsx b/src/components/editor-page/sidebar/specific-sidebar-entries/share-sidebar-entry.tsx index 407a20dbe..fe8c528de 100644 --- a/src/components/editor-page/sidebar/specific-sidebar-entries/share-sidebar-entry.tsx +++ b/src/components/editor-page/sidebar/specific-sidebar-entries/share-sidebar-entry.tsx @@ -1,25 +1,26 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { Fragment, useState } from 'react' +import React, { Fragment } from 'react' import { Trans, useTranslation } from 'react-i18next' import { ShareModal } from '../../document-bar/share/share-modal' import { SidebarButton } from '../sidebar-button/sidebar-button' import type { SpecificSidebarEntryProps } from '../types' +import { useBooleanState } from '../../../../hooks/common/use-boolean-state' export const ShareSidebarEntry: React.FC = ({ className, hide }) => { - const [showModal, setShowModal] = useState(false) + const [modalVisibility, showModal, closeModal] = useBooleanState() useTranslation() return ( - setShowModal(true)}> + - setShowModal(false)} /> + ) } diff --git a/src/components/history-page/entry-menu/dropdown-item-with-deletion-modal.tsx b/src/components/history-page/entry-menu/dropdown-item-with-deletion-modal.tsx index 60b153198..117d47c9e 100644 --- a/src/components/history-page/entry-menu/dropdown-item-with-deletion-modal.tsx +++ b/src/components/history-page/entry-menu/dropdown-item-with-deletion-modal.tsx @@ -1,16 +1,17 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { Fragment, useCallback, useState } from 'react' +import React, { Fragment, useCallback } from 'react' import { Dropdown } from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon' import type { IconName } from '../../common/fork-awesome/types' import type { DeleteHistoryNoteModalProps } from '../../editor-page/sidebar/delete-note-sidebar-entry/delete-note-modal' import { DeleteNoteModal } from '../../editor-page/sidebar/delete-note-sidebar-entry/delete-note-modal' +import { useBooleanState } from '../../../hooks/common/use-boolean-state' export interface DropdownItemWithDeletionModalProps { onConfirm: () => void @@ -47,24 +48,24 @@ export const DropdownItemWithDeletionModal: React.FC< className }) => { useTranslation() - const [showDialog, setShowDialog] = useState(false) + const [modalVisibility, showModal, closeModal] = useBooleanState() + const handleConfirm = useCallback(() => { - setShowDialog(false) + closeModal() onConfirm() - }, [onConfirm]) - const onHide = useCallback(() => setShowDialog(false), []) + }, [closeModal, onConfirm]) return ( - setShowDialog(true)} className={className}> + { const { t } = useTranslation() - const [show, setShow] = useState(false) - - const handleShow = () => setShow(true) - const handleClose = () => setShow(false) + const [modalVisibility, showModal, closeModal] = useBooleanState() const onConfirm = useCallback(() => { deleteAllHistoryEntries().catch((error: Error) => { showErrorNotification('landing.history.error.deleteEntry.text')(error) safeRefreshHistoryState() }) - handleClose() - }, []) + closeModal() + }, [closeModal]) return (
diff --git a/src/components/landing-layout/footer/version-info/version-info-link.tsx b/src/components/landing-layout/footer/version-info/version-info-link.tsx index 4bf8bf26f..86e9f8a63 100644 --- a/src/components/landing-layout/footer/version-info/version-info-link.tsx +++ b/src/components/landing-layout/footer/version-info/version-info-link.tsx @@ -1,25 +1,24 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { Fragment, useCallback, useState } from 'react' +import React, { Fragment } from 'react' import { Trans } from 'react-i18next' import { VersionInfoModal } from './version-info-modal' import { cypressId } from '../../../../utils/cypress-attribute' +import { useBooleanState } from '../../../../hooks/common/use-boolean-state' export const VersionInfoLink: React.FC = () => { - const [show, setShow] = useState(false) - const closeModal = useCallback(() => setShow(false), []) - const showModal = useCallback(() => setShow(true), []) + const [modalVisibility, showModal, closeModal] = useBooleanState() return ( - + ) } diff --git a/src/components/profile-page/access-tokens/access-token-deletion-modal.tsx b/src/components/profile-page/access-tokens/access-token-deletion-modal.tsx index b317bc4ec..ee567167b 100644 --- a/src/components/profile-page/access-tokens/access-token-deletion-modal.tsx +++ b/src/components/profile-page/access-tokens/access-token-deletion-modal.tsx @@ -37,11 +37,7 @@ export const AccessTokenDeletionModal: React.FC = ) }) .catch(showErrorNotification('profile.modal.deleteAccessToken.failed')) - .finally(() => { - if (onHide) { - onHide() - } - }) + .finally(() => onHide?.()) }, [token, onHide]) return ( diff --git a/src/components/profile-page/access-tokens/access-token-list-entry.tsx b/src/components/profile-page/access-tokens/access-token-list-entry.tsx index b4a76e8ee..a1f2cf06d 100644 --- a/src/components/profile-page/access-tokens/access-token-list-entry.tsx +++ b/src/components/profile-page/access-tokens/access-token-list-entry.tsx @@ -1,10 +1,10 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { useCallback, useMemo, useState } from 'react' +import React, { useCallback, useMemo } from 'react' import { Col, ListGroup, Row } from 'react-bootstrap' import { cypressId } from '../../../utils/cypress-attribute' import { Trans, useTranslation } from 'react-i18next' @@ -13,6 +13,7 @@ import { IconButton } from '../../common/icon-button/icon-button' import type { AccessToken } from '../../../api/tokens/types' import { AccessTokenDeletionModal } from './access-token-deletion-modal' import type { AccessTokenUpdateProps } from './profile-access-tokens' +import { useBooleanState } from '../../../hooks/common/use-boolean-state' export interface AccessTokenListEntryProps { token: AccessToken @@ -28,16 +29,12 @@ export const AccessTokenListEntry: React.FC { useTranslation() - const [showDeletionModal, setShowDeletionModal] = useState(false) - - const onShowDeletionModal = useCallback(() => { - setShowDeletionModal(true) - }, []) + const [modalVisibility, showModal, closeModal] = useBooleanState() const onHideDeletionModal = useCallback(() => { - setShowDeletionModal(false) + closeModal() onUpdateList() - }, [onUpdateList]) + }, [closeModal, onUpdateList]) const lastUsed = useMemo(() => { if (!token.lastUsedAt) { @@ -66,12 +63,12 @@ export const AccessTokenListEntry: React.FC - + ) } diff --git a/src/components/profile-page/account-management/profile-account-management.tsx b/src/components/profile-page/account-management/profile-account-management.tsx index c1d6ce70d..ecb23f08e 100644 --- a/src/components/profile-page/account-management/profile-account-management.tsx +++ b/src/components/profile-page/account-management/profile-account-management.tsx @@ -1,30 +1,23 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { Fragment, useCallback, useState } from 'react' +import React, { Fragment } from 'react' import { Button, Card } from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon' import { AccountDeletionModal } from './account-deletion-modal' import { apiUrl } from '../../../utils/api-url' +import { useBooleanState } from '../../../hooks/common/use-boolean-state' /** * Profile page section that allows to export all data from the account or to delete the account. */ export const ProfileAccountManagement: React.FC = () => { useTranslation() - const [showDeleteModal, setShowDeleteModal] = useState(false) - - const onShowDeletionModal = useCallback(() => { - setShowDeleteModal(true) - }, []) - - const onHideDeletionModal = useCallback(() => { - setShowDeleteModal(false) - }, []) + const [modalVisibility, showModal, closeModal] = useBooleanState() return ( @@ -37,13 +30,13 @@ export const ProfileAccountManagement: React.FC = () => { - - + ) } diff --git a/src/hooks/common/use-boolean-state.ts b/src/hooks/common/use-boolean-state.ts new file mode 100644 index 000000000..c252b47f9 --- /dev/null +++ b/src/hooks/common/use-boolean-state.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { useCallback, useState } from 'react' + +/** + * Provides a boolean state and two functions that set the boolean to true or false. + * + * @param initialState The initial value of the state + * @return An array containing the state, and two functions that set the state value to true or false. + */ +export const useBooleanState = ( + initialState: boolean | (() => boolean) = false +): [state: boolean, setToTrue: () => void, setToFalse: () => void] => { + const [state, setState] = useState(initialState) + const setToFalse = useCallback(() => setState(false), []) + const setToTrue = useCallback(() => setState(true), []) + + return [state, setToTrue, setToFalse] +}