Add application state hook (#1308)

* Add application state hook

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Add docs

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2021-06-11 15:21:24 +02:00 committed by GitHub
parent 4720f2d36b
commit 829cc2fe48
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 149 additions and 195 deletions

View file

@ -4,12 +4,10 @@
SPDX-License-Identifier: AGPL-3.0-only
*/
import equal from 'fast-deep-equal'
import React from 'react'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
import React, { useMemo } from 'react'
import { ShowIf } from '../show-if/show-if'
import './branding.scss'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export interface BrandingProps {
inline?: boolean
@ -17,24 +15,30 @@ export interface BrandingProps {
}
export const Branding: React.FC<BrandingProps> = ({ inline = false, delimiter = true }) => {
const branding = useSelector((state: ApplicationState) => state.config.branding, equal)
const branding = useApplicationState((state) => state.config.branding)
const showBranding = !!branding.name || !!branding.logo
const brandingDom = useMemo(() => {
if (branding.logo) {
return (
<ShowIf condition={showBranding}>
<ShowIf condition={delimiter}>
<strong className={`mx-1 ${inline ? 'inline-size' : 'regular-size'}`}>@</strong>
</ShowIf>
{branding.logo ? (
<img
src={branding.logo}
alt={branding.name}
title={branding.name}
className={inline ? 'inline-size' : 'regular-size'}
/>
) : (
branding.name
)}
)
} else {
return branding.name
}
}, [branding.logo, branding.name, inline])
return (
<ShowIf condition={showBranding}>
<ShowIf condition={delimiter}>
<strong className={`mx-1 ${inline ? 'inline-size' : 'regular-size'}`}>@</strong>
</ShowIf>
{brandingDom}
</ShowIf>
)
}

View file

@ -4,17 +4,15 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import equal from 'fast-deep-equal'
import React, { useCallback } from 'react'
import { Alert, Button } from 'react-bootstrap'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
import { setBanner } from '../../../redux/banner/methods'
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
import { BANNER_LOCAL_STORAGE_KEY } from '../../application-loader/initializers/fetch-and-set-banner'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export const MotdBanner: React.FC = () => {
const bannerState = useSelector((state: ApplicationState) => state.banner, equal)
const bannerState = useApplicationState((state) => state.banner)
const dismissBanner = useCallback(() => {
if (bannerState.lastModified) {

View file

@ -6,12 +6,10 @@
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useParams } from 'react-router'
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-title-with-note-title'
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
import { ApplicationState } from '../../redux'
import { setNoteFrontmatter, updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
import { MotdBanner } from '../common/motd-banner/motd-banner'
import { ShowIf } from '../common/show-if/show-if'
@ -23,6 +21,7 @@ import { DocumentInfobar } from './document-infobar'
import { ErrorWhileLoadingNoteAlert } from './ErrorWhileLoadingNoteAlert'
import { LoadingNoteAlert } from './LoadingNoteAlert'
import { RendererType } from '../render-page/rendering-message'
import { useApplicationState } from '../../hooks/common/use-application-state'
export const DocumentReadOnlyPage: React.FC = () => {
useTranslation()
@ -35,7 +34,7 @@ export const DocumentReadOnlyPage: React.FC = () => {
const onFrontmatterChange = useCallback(setNoteFrontmatter, [])
const [error, loading] = useLoadNoteFromServer()
const markdownContent = useNoteMarkdownContent()
const noteDetails = useSelector((state: ApplicationState) => state.noteDetails)
const noteDetails = useApplicationState((state) => state.noteDetails)
return (
<div className={'d-flex flex-column mvh-100 bg-light'}>

View file

@ -5,10 +5,7 @@
*/
import React from 'react'
import equal from 'fast-deep-equal'
import { Nav, Navbar } from 'react-bootstrap'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
import { ShowIf } from '../../common/show-if/show-if'
import { SignInButton } from '../../landing-layout/navigation/sign-in-button'
import { UserDropdown } from '../../landing-layout/navigation/user-dropdown'
@ -21,6 +18,7 @@ import { NoteType } from '../note-frontmatter/note-frontmatter'
import { SlideModeButton } from './slide-mode-button'
import { ReadOnlyModeButton } from './read-only-mode-button'
import { NewNoteButton } from './new-note-button'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export enum AppBarMode {
BASIC,
@ -32,8 +30,8 @@ export interface AppBarProps {
}
export const AppBar: React.FC<AppBarProps> = ({ mode }) => {
const userExists = useSelector((state: ApplicationState) => !!state.user)
const noteFrontmatter = useSelector((state: ApplicationState) => state.noteDetails.frontmatter, equal)
const userExists = useApplicationState((state) => !!state.user)
const noteFrontmatter = useApplicationState((state) => state.noteDetails.frontmatter)
return (
<Navbar bg={'light'}>

View file

@ -7,10 +7,9 @@
import React from 'react'
import { ToggleButton, ToggleButtonGroup } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
import { setEditorMode } from '../../../redux/editor/methods'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export enum EditorMode {
PREVIEW = 'view',
@ -20,7 +19,8 @@ export enum EditorMode {
export const EditorViewMode: React.FC = () => {
const { t } = useTranslation()
const editorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
const editorMode = useApplicationState((state) => state.editorConfig.editorMode)
return (
<ToggleButtonGroup
type='radio'

View file

@ -7,12 +7,11 @@
import React from 'react'
import { ToggleButton, ToggleButtonGroup } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../redux'
import { setEditorSyncScroll } from '../../../../redux/editor/methods'
import { ReactComponent as DisabledScrollIcon } from './disabledScroll.svg'
import { ReactComponent as EnabledScrollIcon } from './enabledScroll.svg'
import './sync-scroll-buttons.scss'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
enum SyncScrollState {
SYNCED,
@ -20,7 +19,7 @@ enum SyncScrollState {
}
export const SyncScrollButtons: React.FC = () => {
const syncScrollEnabled = useSelector((state: ApplicationState) => state.editorConfig.syncScroll)
const syncScrollEnabled = useApplicationState((state) => state.editorConfig.syncScroll)
? SyncScrollState.SYNCED
: SyncScrollState.UNSYNCED
const { t } = useTranslation()

View file

@ -4,19 +4,17 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import equal from 'fast-deep-equal'
import React from 'react'
import { Modal } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useParams } from 'react-router-dom'
import { useFrontendBaseUrl } from '../../../../hooks/common/use-frontend-base-url'
import { ApplicationState } from '../../../../redux'
import { CopyableField } from '../../../common/copyable/copyable-field/copyable-field'
import { CommonModal } from '../../../common/modals/common-modal'
import { ShowIf } from '../../../common/show-if/show-if'
import { EditorPagePathParams } from '../../editor-page'
import { NoteType } from '../../note-frontmatter/note-frontmatter'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
export interface ShareModalProps {
show: boolean
@ -25,8 +23,8 @@ export interface ShareModalProps {
export const ShareModal: React.FC<ShareModalProps> = ({ show, onHide }) => {
useTranslation()
const noteFrontmatter = useSelector((state: ApplicationState) => state.noteDetails.frontmatter, equal)
const editorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
const noteFrontmatter = useApplicationState((state) => state.noteDetails.frontmatter)
const editorMode = useApplicationState((state) => state.editorConfig.editorMode)
const baseUrl = useFrontendBaseUrl()
const { id } = useParams<EditorPagePathParams>()

View file

@ -6,11 +6,9 @@
import React, { useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-title-with-note-title'
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
import { ApplicationState } from '../../redux'
import {
SetCheckboxInMarkdownContent,
setNoteFrontmatter,
@ -36,6 +34,7 @@ import { UiNotifications } from '../notifications/ui-notifications'
import { useNotificationTest } from './use-notification-test'
import { IframeCommunicatorContextProvider } from './render-context/iframe-communicator-context-provider'
import { useUpdateLocalHistoryEntry } from './hooks/useUpdateLocalHistoryEntry'
import { useApplicationState } from '../../hooks/common/use-application-state'
export interface EditorPagePathParams {
id: string
@ -51,8 +50,8 @@ export const EditorPage: React.FC = () => {
const markdownContent = useNoteMarkdownContent()
const scrollSource = useRef<ScrollSource>(ScrollSource.EDITOR)
const editorMode: EditorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
const editorSyncScroll: boolean = useSelector((state: ApplicationState) => state.editorConfig.syncScroll)
const editorMode: EditorMode = useApplicationState((state) => state.editorConfig.editorMode)
const editorSyncScroll: boolean = useApplicationState((state) => state.editorConfig.syncScroll)
const [scrollState, setScrollState] = useState<DualScrollState>(() => ({
editorScrollState: { firstLineInView: 1, scrolledPercentage: 0 },

View file

@ -27,12 +27,9 @@ import 'codemirror/keymap/emacs'
import 'codemirror/keymap/sublime'
import 'codemirror/keymap/vim'
import 'codemirror/mode/gfm/gfm'
import equal from 'fast-deep-equal'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
import { MaxLengthWarningModal } from '../editor-modals/max-length-warning-modal'
import { ScrollProps, ScrollState } from '../synced-scroll/scroll-props'
import { allHinters, findWordAtCursor } from './autocompletion'
@ -42,6 +39,7 @@ import { createStatusInfo, defaultState, StatusBar, StatusBarInfo } from './stat
import { ToolBar } from './tool-bar/tool-bar'
import { handleUpload } from './upload-handler'
import { handleFilePaste, handleTablePaste, PasteEvent } from './tool-bar/utils/pasteHandlers'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export interface EditorPaneProps {
onContentChange: (content: string) => void
@ -81,14 +79,14 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({
onMakeScrollSource
}) => {
const { t } = useTranslation()
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
const smartPasteEnabled = useSelector((state: ApplicationState) => state.editorConfig.smartPaste)
const maxLength = useApplicationState((state) => state.config.maxDocumentLength)
const smartPasteEnabled = useApplicationState((state) => state.editorConfig.smartPaste)
const [showMaxLengthWarning, setShowMaxLengthWarning] = useState(false)
const maxLengthWarningAlreadyShown = useRef(false)
const [editor, setEditor] = useState<Editor>()
const [statusBarInfo, setStatusBarInfo] = useState<StatusBarInfo>(defaultState)
const editorPreferences = useSelector((state: ApplicationState) => state.editorConfig.preferences, equal)
const ligaturesEnabled = useSelector((state: ApplicationState) => state.editorConfig.ligatures, equal)
const editorPreferences = useApplicationState((state) => state.editorConfig.preferences)
const ligaturesEnabled = useApplicationState((state) => state.editorConfig.ligatures)
const lastScrollPosition = useRef<number>()
const [editorScroll, setEditorScroll] = useState<ScrollInfo>()

View file

@ -5,24 +5,19 @@
*/
import { EditorConfiguration } from 'codemirror'
import equal from 'fast-deep-equal'
import React, { ChangeEvent, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../../redux'
import { mergeEditorPreferences } from '../../../../../redux/editor/methods'
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
import { EditorPreferenceProperty } from './editor-preference-property'
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
export interface EditorPreferenceBooleanProps {
property: EditorPreferenceProperty
}
export const EditorPreferenceBooleanProperty: React.FC<EditorPreferenceBooleanProps> = ({ property }) => {
const preference = useSelector(
(state: ApplicationState) => state.editorConfig.preferences[property]?.toString() || '',
equal
)
const preference = useApplicationState((state) => state.editorConfig.preferences[property]?.toString() ?? '')
const { t } = useTranslation()
const selectItem = useCallback(

View file

@ -5,13 +5,12 @@
*/
import React, { ChangeEvent, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../../redux'
import { setEditorLigatures } from '../../../../../redux/editor/methods'
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
export const EditorPreferenceLigaturesSelect: React.FC = () => {
const ligaturesEnabled = useSelector((state: ApplicationState) => Boolean(state.editorConfig.ligatures).toString())
const ligaturesEnabled = useApplicationState((state) => Boolean(state.editorConfig.ligatures).toString())
const saveLigatures = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
const ligaturesActivated: boolean = event.target.value === 'true'
setEditorLigatures(ligaturesActivated)

View file

@ -5,23 +5,18 @@
*/
import { EditorConfiguration } from 'codemirror'
import equal from 'fast-deep-equal'
import React, { ChangeEvent, useCallback } from 'react'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../../redux'
import { mergeEditorPreferences } from '../../../../../redux/editor/methods'
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
import { EditorPreferenceProperty } from './editor-preference-property'
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
export interface EditorPreferenceNumberProps {
property: EditorPreferenceProperty
}
export const EditorPreferenceNumberProperty: React.FC<EditorPreferenceNumberProps> = ({ property }) => {
const preference = useSelector(
(state: ApplicationState) => state.editorConfig.preferences[property]?.toString() || '',
equal
)
const preference = useApplicationState((state) => state.editorConfig.preferences[property]?.toString() ?? '')
const selectItem = useCallback(
(event: ChangeEvent<HTMLSelectElement>) => {

View file

@ -5,14 +5,12 @@
*/
import { EditorConfiguration } from 'codemirror'
import equal from 'fast-deep-equal'
import React, { ChangeEvent, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../../redux'
import { mergeEditorPreferences } from '../../../../../redux/editor/methods'
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
import { EditorPreferenceProperty } from './editor-preference-property'
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
export interface EditorPreferenceSelectPropertyProps {
property: EditorPreferenceProperty
@ -23,10 +21,7 @@ export const EditorPreferenceSelectProperty: React.FC<EditorPreferenceSelectProp
property,
selections
}) => {
const preference = useSelector(
(state: ApplicationState) => state.editorConfig.preferences[property]?.toString() || '',
equal
)
const preference = useApplicationState((state) => state.editorConfig.preferences[property]?.toString() ?? '')
const { t } = useTranslation()

View file

@ -5,13 +5,12 @@
*/
import React, { ChangeEvent, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../../redux'
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
import { setEditorSmartPaste } from '../../../../../redux/editor/methods'
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
export const EditorPreferenceSmartPasteSelect: React.FC = () => {
const smartPasteEnabled = useSelector((state: ApplicationState) => Boolean(state.editorConfig.smartPaste).toString())
const smartPasteEnabled = useApplicationState((state) => Boolean(state.editorConfig.smartPaste).toString())
const saveSmartPaste = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
const smartPasteActivated: boolean = event.target.value === 'true'
setEditorSmartPaste(smartPasteActivated)

View file

@ -4,12 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import equal from 'fast-deep-equal'
import React, { Fragment, useState } from 'react'
import { Button, Form, ListGroup } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../../redux'
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
import { CommonModal } from '../../../../common/modals/common-modal'
import { ShowIf } from '../../../../common/show-if/show-if'
@ -20,14 +17,12 @@ import { EditorPreferenceNumberProperty } from './editor-preference-number-prope
import { EditorPreferenceProperty } from './editor-preference-property'
import { EditorPreferenceSelectProperty } from './editor-preference-select-property'
import { EditorPreferenceSmartPasteSelect } from './editor-preference-smart-paste-select'
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
export const EditorPreferences: React.FC = () => {
const { t } = useTranslation()
const [showModal, setShowModal] = useState(false)
const indentWithTabs = useSelector(
(state: ApplicationState) => state.editorConfig.preferences.indentWithTabs ?? false,
equal
)
const indentWithTabs = useApplicationState((state) => state.editorConfig.preferences.indentWithTabs ?? false)
return (
<Fragment>

View file

@ -6,18 +6,18 @@
import equal from 'fast-deep-equal'
import { useEffect, useRef } from 'react'
import { useSelector } from 'react-redux'
import { ApplicationState, store } from '../../../redux'
import { store } from '../../../redux'
import { useParams } from 'react-router-dom'
import { EditorPagePathParams } from '../editor-page'
import { HistoryEntry, HistoryEntryOrigin } from '../../../redux/history/types'
import { updateLocalHistoryEntry } from '../../../redux/history/methods'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export const useUpdateLocalHistoryEntry = (updateReady: boolean): void => {
const { id } = useParams<EditorPagePathParams>()
const userExists = useSelector((state: ApplicationState) => !!state.user)
const currentNoteTitle = useSelector((state: ApplicationState) => state.noteDetails.noteTitle)
const currentNoteTags = useSelector((state: ApplicationState) => state.noteDetails.frontmatter.tags)
const userExists = useApplicationState((state) => !!state.user)
const currentNoteTitle = useApplicationState((state) => state.noteDetails.noteTitle)
const currentNoteTags = useApplicationState((state) => state.noteDetails.frontmatter.tags)
const lastNoteTitle = useRef('')
const lastNoteTags = useRef<string[]>([])

View file

@ -5,9 +5,8 @@
*/
import equal from 'fast-deep-equal'
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { useSelector } from 'react-redux'
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-activated'
import { ApplicationState } from '../../../redux'
import { isTestMode } from '../../../utils/test-modes'
import { RendererProps } from '../../render-page/markdown-document'
import { ImageDetails, RendererType } from '../../render-page/rendering-message'
@ -42,7 +41,7 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
const frameReference = useRef<HTMLIFrameElement>(null)
const rendererOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.rendererOrigin)
const rendererOrigin = useApplicationState((state) => state.config.iframeCommunication.rendererOrigin)
const renderPageUrl = `${rendererOrigin}render`
const resetRendererReady = useCallback(() => setRendererReady(false), [])
const iframeCommunicator = useContextOrStandaloneIframeCommunicator()

View file

@ -7,17 +7,14 @@
import React from 'react'
import { Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import links from '../../../links.json'
import { ApplicationState } from '../../../redux'
import { TranslatedExternalLink } from '../../common/links/translated-external-link'
import { ShowIf } from '../../common/show-if/show-if'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export const YamlArrayDeprecationAlert: React.FC = () => {
useTranslation()
const yamlDeprecatedTags = useSelector(
(state: ApplicationState) => state.noteDetails.frontmatter.deprecatedTagsSyntax
)
const yamlDeprecatedTags = useApplicationState((state) => state.noteDetails.frontmatter.deprecatedTagsSyntax)
return (
<ShowIf condition={yamlDeprecatedTags}>

View file

@ -10,15 +10,14 @@ import { SidebarButton } from './sidebar-button'
import { SpecificSidebarEntryProps } from './types'
import { useParams } from 'react-router-dom'
import { EditorPagePathParams } from '../editor-page'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
import { toggleHistoryEntryPinning } from '../../../redux/history/methods'
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export const PinNoteSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
const { t } = useTranslation()
const { id } = useParams<EditorPagePathParams>()
const history = useSelector((state: ApplicationState) => state.history)
const history = useApplicationState((state) => state.history)
const isPinned = useMemo(() => {
const entry = history.find((entry) => entry.identifier === id)

View file

@ -13,8 +13,7 @@ import { DeleteNoteItem } from './delete-note-item'
import './entry-menu.scss'
import { RemoveNoteEntryItem } from './remove-note-entry-item'
import { HistoryEntryOrigin } from '../../../redux/history/types'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export interface EntryMenuProps {
id: string
@ -29,7 +28,7 @@ export interface EntryMenuProps {
export const EntryMenu: React.FC<EntryMenuProps> = ({ id, title, origin, isDark, onRemove, onDelete, className }) => {
useTranslation()
const userExists = useSelector((state: ApplicationState) => !!state.user)
const userExists = useApplicationState((state) => !!state.user)
return (
<Dropdown className={`d-inline-flex ${className || ''}`}>

View file

@ -7,19 +7,18 @@
import React, { Fragment, useEffect, useMemo, useState } from 'react'
import { Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
import { HistoryContent } from './history-content/history-content'
import { HistoryToolbar, HistoryToolbarState, initToolbarState } from './history-toolbar/history-toolbar'
import { sortAndFilterEntries } from './utils'
import { refreshHistoryState } from '../../redux/history/methods'
import { HistoryEntry } from '../../redux/history/types'
import { showErrorNotification } from '../../redux/ui-notifications/methods'
import { useApplicationState } from '../../hooks/common/use-application-state'
export const HistoryPage: React.FC = () => {
const { t } = useTranslation()
const allEntries = useSelector((state: ApplicationState) => state.history)
const allEntries = useApplicationState((state) => state.history)
const [toolbarState, setToolbarState] = useState<HistoryToolbarState>(initToolbarState)
const entriesToShow = useMemo<HistoryEntry[]>(

View file

@ -9,9 +9,7 @@ import React, { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState }
import { Button, Form, FormControl, InputGroup, ToggleButton, ToggleButtonGroup } from 'react-bootstrap'
import { Typeahead } from 'react-bootstrap-typeahead'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useQueryState } from 'react-router-use-location-state'
import { ApplicationState } from '../../../redux'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { ShowIf } from '../../common/show-if/show-if'
import { SortButton, SortModeEnum } from '../sort-button/sort-button'
@ -22,6 +20,7 @@ import './typeahead-hacks.scss'
import { HistoryEntryOrigin } from '../../../redux/history/types'
import { importHistoryEntries, refreshHistoryState, setHistoryEntries } from '../../../redux/history/methods'
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export type HistoryToolbarChange = (newState: HistoryToolbarState) => void
@ -61,8 +60,8 @@ export const initToolbarState: HistoryToolbarState = {
export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange }) => {
const { t } = useTranslation()
const historyEntries = useSelector((state: ApplicationState) => state.history)
const userExists = useSelector((state: ApplicationState) => !!state.user)
const historyEntries = useApplicationState((state) => state.history)
const userExists = useApplicationState((state) => !!state.user)
const tags = useMemo<string[]>(() => {
const allTags = historyEntries.map((entry) => entry.tags).flat()

View file

@ -16,14 +16,13 @@ import {
mergeHistoryEntries,
refreshHistoryState
} from '../../../redux/history/methods'
import { ApplicationState } from '../../../redux'
import { useSelector } from 'react-redux'
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export const ImportHistoryButton: React.FC = () => {
const { t } = useTranslation()
const userExists = useSelector((state: ApplicationState) => !!state.user)
const historyState = useSelector((state: ApplicationState) => state.history)
const userExists = useApplicationState((state) => !!state.user)
const historyState = useApplicationState((state) => state.history)
const uploadInput = useRef<HTMLInputElement>(null)
const [show, setShow] = useState(false)
const [fileName, setFilename] = useState('')

View file

@ -7,17 +7,16 @@
import React from 'react'
import { Button } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { ApplicationState } from '../../../redux'
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { ShowIf } from '../../common/show-if/show-if'
import { SignInButton } from '../../landing-layout/navigation/sign-in-button'
import './cover-buttons.scss'
export const CoverButtons: React.FC = () => {
useTranslation()
const userExists = useSelector((state: ApplicationState) => !!state.user)
const anyAuthProviderActivated = useSelector((state: ApplicationState) =>
const userExists = useApplicationState((state) => !!state.user)
const anyAuthProviderActivated = useApplicationState((state) =>
Object.values(state.config.authProviders).includes(true)
)

View file

@ -6,21 +6,18 @@
import React, { Fragment } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import links from '../../../links.json'
import { ApplicationState } from '../../../redux'
import { ExternalLink } from '../../common/links/external-link'
import { TranslatedExternalLink } from '../../common/links/translated-external-link'
import { TranslatedInternalLink } from '../../common/links/translated-internal-link'
import { VersionInfoLink } from './version-info/version-info-link'
import equal from 'fast-deep-equal'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export const PoweredByLinks: React.FC = () => {
useTranslation()
const specialUrls: [string, string][] = useSelector(
(state: ApplicationState) => Object.entries(state.config.specialUrls) as [string, string][],
equal
const specialUrls: [string, string][] = useApplicationState((state) =>
Object.entries(state.config.specialUrls).map(([i18nkey, url]) => [i18nkey, String(url)])
)
return (

View file

@ -10,13 +10,11 @@ import { Modal, Row } from 'react-bootstrap'
import { VersionInfoModalColumn } from './version-info-modal-column'
import frontendVersion from '../../../../version.json'
import links from '../../../../links.json'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../redux'
import equal from 'fast-deep-equal'
import { BackendVersion } from '../../../../api/config/types'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
export const VersionInfoModal: React.FC<CommonModalProps> = ({ onHide, show }) => {
const serverVersion: BackendVersion = useSelector((state: ApplicationState) => state.config.version, equal)
const serverVersion: BackendVersion = useApplicationState((state) => state.config.version)
const backendVersion = useMemo(() => {
const version = `${serverVersion.major}.${serverVersion.minor}.${serverVersion.patch}`

View file

@ -7,8 +7,7 @@
import React, { Fragment } from 'react'
import { Navbar } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../redux'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
import { HeaderNavLink } from '../header-nav-link'
import { NewGuestNoteButton } from '../new-guest-note-button'
import { NewUserNoteButton } from '../new-user-note-button'
@ -18,7 +17,7 @@ import './header-bar.scss'
const HeaderBar: React.FC = () => {
useTranslation()
const userExists = useSelector((state: ApplicationState) => !!state.user)
const userExists = useApplicationState((state) => !!state.user)
return (
<Navbar className='justify-content-between'>

View file

@ -4,23 +4,21 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import equal from 'fast-deep-equal'
import React, { useMemo } from 'react'
import { Button } from 'react-bootstrap'
import { ButtonProps } from 'react-bootstrap/Button'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { LinkContainer } from 'react-router-bootstrap'
import { ApplicationState } from '../../../redux'
import { ShowIf } from '../../common/show-if/show-if'
import { getApiUrl } from '../../../api/utils'
import { INTERACTIVE_LOGIN_METHODS } from '../../../api/auth'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export type SignInButtonProps = Omit<ButtonProps, 'href'>
export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props }) => {
const { t } = useTranslation()
const authProviders = useSelector((state: ApplicationState) => state.config.authProviders, equal)
const authProviders = useApplicationState((state) => state.config.authProviders)
const authEnabled = useMemo(() => Object.values(authProviders).includes(true), [authProviders])
const loginLink = useMemo(() => {

View file

@ -4,20 +4,18 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import equal from 'fast-deep-equal'
import React from 'react'
import { Dropdown } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { LinkContainer } from 'react-router-bootstrap'
import { ApplicationState } from '../../../redux'
import { clearUser } from '../../../redux/user/methods'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { UserAvatar } from '../../common/user-avatar/user-avatar'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export const UserDropdown: React.FC = () => {
useTranslation()
const user = useSelector((state: ApplicationState) => state.user, equal)
const user = useApplicationState((state) => state.user)
if (!user) {
return null

View file

@ -7,19 +7,18 @@
import React, { FormEvent, useCallback, useState } from 'react'
import { Alert, Button, Card, Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { doInternalLogin } from '../../../api/auth'
import { ApplicationState } from '../../../redux'
import { ShowIf } from '../../common/show-if/show-if'
import { fetchAndSetUser } from './utils'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export const ViaInternal: React.FC = () => {
const { t } = useTranslation()
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(false)
const allowRegister = useSelector((state: ApplicationState) => state.config.allowRegister)
const allowRegister = useApplicationState((state) => state.config.allowRegister)
const onLoginSubmit = useCallback(
(event: FormEvent) => {

View file

@ -8,14 +8,13 @@ import React, { FormEvent, useCallback, useState } from 'react'
import { Alert, Button, Card, Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { doLdapLogin } from '../../../api/auth'
import { ApplicationState } from '../../../redux'
import { fetchAndSetUser } from './utils'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export const ViaLdap: React.FC = () => {
const { t } = useTranslation()
const ldapCustomName = useSelector((state: ApplicationState) => state.config.customAuthNames.ldap)
const ldapCustomName = useApplicationState((state) => state.config.customAuthNames.ldap)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')

View file

@ -5,8 +5,7 @@
*/
import React from 'react'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { IconName } from '../../common/fork-awesome/types'
import { SocialLinkButton } from './social-link-button/social-link-button'
@ -108,7 +107,7 @@ export interface ViaOneClickProps {
}
export const ViaOneClick: React.FC<ViaOneClickProps> = ({ oneClickType, optionalName }) => {
const backendUrl = useSelector((state: ApplicationState) => state.apiUrl.apiUrl)
const backendUrl = useApplicationState((state) => state.apiUrl.apiUrl)
const { name, icon, className, url } = getMetadata(backendUrl, oneClickType)
const text = optionalName || name

View file

@ -4,25 +4,23 @@
SPDX-License-Identifier: AGPL-3.0-only
*/
import equal from 'fast-deep-equal'
import React, { Fragment } from 'react'
import { Card, Col, Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { Redirect } from 'react-router'
import { ApplicationState } from '../../redux'
import { ShowIf } from '../common/show-if/show-if'
import { ViaInternal } from './auth/via-internal'
import { ViaLdap } from './auth/via-ldap'
import { OneClickType, ViaOneClick } from './auth/via-one-click'
import { ViaOpenId } from './auth/via-openid'
import { useApplicationState } from '../../hooks/common/use-application-state'
export const LoginPage: React.FC = () => {
useTranslation()
const authProviders = useSelector((state: ApplicationState) => state.config.authProviders, equal)
const customSamlAuthName = useSelector((state: ApplicationState) => state.config.customAuthNames.saml)
const customOauthAuthName = useSelector((state: ApplicationState) => state.config.customAuthNames.oauth2)
const userLoggedIn = useSelector((state: ApplicationState) => !!state.user)
const authProviders = useApplicationState((state) => state.config.authProviders)
const customSamlAuthName = useApplicationState((state) => state.config.customAuthNames.saml)
const customOauthAuthName = useApplicationState((state) => state.config.customAuthNames.oauth2)
const userLoggedIn = useApplicationState((state) => !!state.user)
const oneClickProviders = [
authProviders.dropbox,

View file

@ -7,15 +7,14 @@
import React from 'react'
import { Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
import { useApplicationState } from '../../hooks/common/use-application-state'
import { ShowIf } from '../common/show-if/show-if'
import { SimpleAlertProps } from '../common/simple-alert/simple-alert-props'
export const DocumentLengthLimitReachedAlert: React.FC<SimpleAlertProps> = ({ show }) => {
useTranslation()
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
const maxLength = useApplicationState((state) => state.config.maxDocumentLength)
return (
<ShowIf condition={show}>

View file

@ -4,12 +4,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
import { useMemo } from 'react'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export const useTrimmedContent = (content: string): [trimmedContent: string, contentExceedsLimit: boolean] => {
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
const maxLength = useApplicationState((state) => state.config.maxDocumentLength)
const contentExceedsLimit = content.length > maxLength
const trimmedContent = useMemo(

View file

@ -5,13 +5,12 @@
*/
import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { getProxiedUrl } from '../../../../api/media'
import { ApplicationState } from '../../../../redux'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
export const ProxyImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = ({ src, title, alt, ...props }) => {
const [imageUrl, setImageUrl] = useState('')
const imageProxyEnabled = useSelector((state: ApplicationState) => state.config.useImageProxy)
const imageProxyEnabled = useApplicationState((state) => state.config.useImageProxy)
useEffect(() => {
if (!imageProxyEnabled || !src) {

View file

@ -7,12 +7,10 @@
import React from 'react'
import { UiNotificationToast } from './ui-notification-toast'
import './notifications.scss'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
import equal from 'fast-deep-equal'
import { useApplicationState } from '../../hooks/common/use-application-state'
export const UiNotifications: React.FC = () => {
const notifications = useSelector((state: ApplicationState) => state.uiNotifications, equal)
const notifications = useApplicationState((state) => state.uiNotifications)
return (
<div className={'notifications-area'} aria-live='polite' aria-atomic='true'>

View file

@ -6,9 +6,8 @@
import React, { Fragment } from 'react'
import { Col, Row } from 'react-bootstrap'
import { useSelector } from 'react-redux'
import { Redirect } from 'react-router'
import { ApplicationState } from '../../redux'
import { useApplicationState } from '../../hooks/common/use-application-state'
import { LoginProvider } from '../../redux/user/types'
import { ShowIf } from '../common/show-if/show-if'
import { ProfileAccessTokens } from './access-tokens/profile-access-tokens'
@ -17,7 +16,7 @@ import { ProfileChangePassword } from './settings/profile-change-password'
import { ProfileDisplayName } from './settings/profile-display-name'
export const ProfilePage: React.FC = () => {
const userProvider = useSelector((state: ApplicationState) => state.user?.provider)
const userProvider = useApplicationState((state) => state.user?.provider)
if (!userProvider) {
return <Redirect to={'/login'} />

View file

@ -7,15 +7,14 @@
import React, { ChangeEvent, FormEvent, useEffect, useState } from 'react'
import { Alert, Button, Card, Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { updateDisplayName } from '../../../api/me'
import { ApplicationState } from '../../../redux'
import { fetchAndSetUser } from '../../login-page/auth/utils'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export const ProfileDisplayName: React.FC = () => {
const regexInvalidDisplayName = /^\s*$/
const { t } = useTranslation()
const userName = useSelector((state: ApplicationState) => state.user?.name)
const userName = useApplicationState((state) => state.user?.name)
const [submittable, setSubmittable] = useState(false)
const [error, setError] = useState(false)
const [displayName, setDisplayName] = useState('')

View file

@ -7,10 +7,9 @@
import React, { FormEvent, Fragment, useCallback, useEffect, useState } from 'react'
import { Alert, Button, Card, Col, Form, Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { Redirect } from 'react-router'
import { doInternalRegister } from '../../api/auth'
import { ApplicationState } from '../../redux'
import { useApplicationState } from '../../hooks/common/use-application-state'
import { TranslatedExternalLink } from '../common/links/translated-external-link'
import { ShowIf } from '../common/show-if/show-if'
import { fetchAndSetUser } from '../login-page/auth/utils'
@ -23,9 +22,9 @@ export enum RegisterError {
export const RegisterPage: React.FC = () => {
const { t } = useTranslation()
const allowRegister = useSelector((state: ApplicationState) => state.config.allowRegister)
const specialUrls = useSelector((state: ApplicationState) => state.config.specialUrls)
const userExists = useSelector((state: ApplicationState) => !!state.user)
const allowRegister = useApplicationState((state) => state.config.allowRegister)
const specialUrls = useApplicationState((state) => state.config.specialUrls)
const userExists = useApplicationState((state) => !!state.user)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')

View file

@ -14,10 +14,9 @@ import { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
import { BasicMarkdownRenderer } from '../markdown-renderer/basic-markdown-renderer'
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
import './markdown-document.scss'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
import { WidthBasedTableOfContents } from './width-based-table-of-contents'
import { ShowIf } from '../common/show-if/show-if'
import { useApplicationState } from '../../hooks/common/use-application-state'
export interface RendererProps extends ScrollProps {
onFirstHeadingChange?: (firstHeading: string | undefined) => void
@ -60,7 +59,7 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
const [tocAst, setTocAst] = useState<TocAst>()
const useAlternativeBreaks = useSelector((state: ApplicationState) => state.noteDetails.frontmatter.breaks)
const useAlternativeBreaks = useApplicationState((state) => state.noteDetails.frontmatter.breaks)
useEffect(() => {
if (!onHeightChange) {

View file

@ -4,9 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useSelector } from 'react-redux'
import { useApplicationState } from '../../hooks/common/use-application-state'
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
import { ApplicationState } from '../../redux'
import { setDarkMode } from '../../redux/dark-mode/methods'
import { setNoteFrontmatter } from '../../redux/note-details/methods'
import { NoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter'
@ -24,7 +23,7 @@ export const RenderPage: React.FC = () => {
const [scrollState, setScrollState] = useState<ScrollState>({ firstLineInView: 1, scrolledPercentage: 0 })
const [baseConfiguration, setBaseConfiguration] = useState<BaseConfiguration | undefined>(undefined)
const editorOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.editorOrigin)
const editorOrigin = useApplicationState((state) => state.config.iframeCommunication.editorOrigin)
const iframeCommunicator = useMemo(() => {
const newCommunicator = new IframeRendererToEditorCommunicator()

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
import equal from 'fast-deep-equal'
/**
* Accesses the global application state to retrieve information.
*
* @param selector A selector function that extracts the needed information from the state.
* @param checkForEquality An optional custom equality function. If not provided then {@link equal equal from fast-deep-equal} will be used.
*/
export const useApplicationState = <TSelected>(
selector: (state: ApplicationState) => TSelected,
checkForEquality?: (a: TSelected, b: TSelected) => boolean
): TSelected => {
return useSelector<ApplicationState, TSelected>(selector, checkForEquality ? checkForEquality : equal)
}

View file

@ -5,14 +5,13 @@
*/
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
import { useApplicationState } from './use-application-state'
import { useDocumentTitle } from './use-document-title'
export const useDocumentTitleWithNoteTitle = (): void => {
const { t } = useTranslation()
const untitledNote = t('editor.untitledNote')
const noteTitle = useSelector((state: ApplicationState) => state.noteDetails.noteTitle)
const noteTitle = useApplicationState((state) => state.noteDetails.noteTitle)
useDocumentTitle(noteTitle === '' ? untitledNote : noteTitle)
}

View file

@ -5,11 +5,10 @@
*/
import { useEffect } from 'react'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
import { useApplicationState } from './use-application-state'
export const useDocumentTitle = (title?: string): void => {
const brandingName = useSelector((state: ApplicationState) => state.config.branding.name)
const brandingName = useApplicationState((state) => state.config.branding.name)
useEffect(() => {
document.title = `${title ? title + ' - ' : ''}HedgeDoc ${brandingName ? ` @ ${brandingName}` : ''}`

View file

@ -4,9 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
import { useApplicationState } from './use-application-state'
export const useIsDarkModeActivated = (): boolean => {
return useSelector((state: ApplicationState) => state.darkMode.darkMode)
return useApplicationState((state) => state.darkMode.darkMode)
}

View file

@ -4,9 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
import { useApplicationState } from './use-application-state'
export const useNoteMarkdownContent = (): string => {
return useSelector((state: ApplicationState) => state.noteDetails.markdownContent)
return useApplicationState((state) => state.noteDetails.markdownContent)
}