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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,15 +10,14 @@ import { SidebarButton } from './sidebar-button'
import { SpecificSidebarEntryProps } from './types' import { SpecificSidebarEntryProps } from './types'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { EditorPagePathParams } from '../editor-page' import { EditorPagePathParams } from '../editor-page'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
import { toggleHistoryEntryPinning } from '../../../redux/history/methods' import { toggleHistoryEntryPinning } from '../../../redux/history/methods'
import { showErrorNotification } from '../../../redux/ui-notifications/methods' import { showErrorNotification } from '../../../redux/ui-notifications/methods'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export const PinNoteSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => { export const PinNoteSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { id } = useParams<EditorPagePathParams>() const { id } = useParams<EditorPagePathParams>()
const history = useSelector((state: ApplicationState) => state.history) const history = useApplicationState((state) => state.history)
const isPinned = useMemo(() => { const isPinned = useMemo(() => {
const entry = history.find((entry) => entry.identifier === id) 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 './entry-menu.scss'
import { RemoveNoteEntryItem } from './remove-note-entry-item' import { RemoveNoteEntryItem } from './remove-note-entry-item'
import { HistoryEntryOrigin } from '../../../redux/history/types' import { HistoryEntryOrigin } from '../../../redux/history/types'
import { useSelector } from 'react-redux' import { useApplicationState } from '../../../hooks/common/use-application-state'
import { ApplicationState } from '../../../redux'
export interface EntryMenuProps { export interface EntryMenuProps {
id: string id: string
@ -29,7 +28,7 @@ export interface EntryMenuProps {
export const EntryMenu: React.FC<EntryMenuProps> = ({ id, title, origin, isDark, onRemove, onDelete, className }) => { export const EntryMenu: React.FC<EntryMenuProps> = ({ id, title, origin, isDark, onRemove, onDelete, className }) => {
useTranslation() useTranslation()
const userExists = useSelector((state: ApplicationState) => !!state.user) const userExists = useApplicationState((state) => !!state.user)
return ( return (
<Dropdown className={`d-inline-flex ${className || ''}`}> <Dropdown className={`d-inline-flex ${className || ''}`}>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,10 +7,9 @@
import React, { FormEvent, Fragment, useCallback, useEffect, useState } from 'react' import React, { FormEvent, Fragment, useCallback, useEffect, useState } from 'react'
import { Alert, Button, Card, Col, Form, Row } from 'react-bootstrap' import { Alert, Button, Card, Col, Form, Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { Redirect } from 'react-router' import { Redirect } from 'react-router'
import { doInternalRegister } from '../../api/auth' 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 { TranslatedExternalLink } from '../common/links/translated-external-link'
import { ShowIf } from '../common/show-if/show-if' import { ShowIf } from '../common/show-if/show-if'
import { fetchAndSetUser } from '../login-page/auth/utils' import { fetchAndSetUser } from '../login-page/auth/utils'
@ -23,9 +22,9 @@ export enum RegisterError {
export const RegisterPage: React.FC = () => { export const RegisterPage: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const allowRegister = useSelector((state: ApplicationState) => state.config.allowRegister) const allowRegister = useApplicationState((state) => state.config.allowRegister)
const specialUrls = useSelector((state: ApplicationState) => state.config.specialUrls) const specialUrls = useApplicationState((state) => state.config.specialUrls)
const userExists = useSelector((state: ApplicationState) => !!state.user) const userExists = useApplicationState((state) => !!state.user)
const [username, setUsername] = useState('') const [username, setUsername] = useState('')
const [password, setPassword] = 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 { BasicMarkdownRenderer } from '../markdown-renderer/basic-markdown-renderer'
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer' import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
import './markdown-document.scss' import './markdown-document.scss'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
import { WidthBasedTableOfContents } from './width-based-table-of-contents' import { WidthBasedTableOfContents } from './width-based-table-of-contents'
import { ShowIf } from '../common/show-if/show-if' import { ShowIf } from '../common/show-if/show-if'
import { useApplicationState } from '../../hooks/common/use-application-state'
export interface RendererProps extends ScrollProps { export interface RendererProps extends ScrollProps {
onFirstHeadingChange?: (firstHeading: string | undefined) => void onFirstHeadingChange?: (firstHeading: string | undefined) => void
@ -60,7 +59,7 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
const [tocAst, setTocAst] = useState<TocAst>() const [tocAst, setTocAst] = useState<TocAst>()
const useAlternativeBreaks = useSelector((state: ApplicationState) => state.noteDetails.frontmatter.breaks) const useAlternativeBreaks = useApplicationState((state) => state.noteDetails.frontmatter.breaks)
useEffect(() => { useEffect(() => {
if (!onHeightChange) { if (!onHeightChange) {

View file

@ -4,9 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { useCallback, useEffect, useMemo, useState } from 'react' 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 { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
import { ApplicationState } from '../../redux'
import { setDarkMode } from '../../redux/dark-mode/methods' import { setDarkMode } from '../../redux/dark-mode/methods'
import { setNoteFrontmatter } from '../../redux/note-details/methods' import { setNoteFrontmatter } from '../../redux/note-details/methods'
import { NoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter' 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 [scrollState, setScrollState] = useState<ScrollState>({ firstLineInView: 1, scrolledPercentage: 0 })
const [baseConfiguration, setBaseConfiguration] = useState<BaseConfiguration | undefined>(undefined) 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 iframeCommunicator = useMemo(() => {
const newCommunicator = new IframeRendererToEditorCommunicator() 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 { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux' import { useApplicationState } from './use-application-state'
import { ApplicationState } from '../../redux'
import { useDocumentTitle } from './use-document-title' import { useDocumentTitle } from './use-document-title'
export const useDocumentTitleWithNoteTitle = (): void => { export const useDocumentTitleWithNoteTitle = (): void => {
const { t } = useTranslation() const { t } = useTranslation()
const untitledNote = t('editor.untitledNote') const untitledNote = t('editor.untitledNote')
const noteTitle = useSelector((state: ApplicationState) => state.noteDetails.noteTitle) const noteTitle = useApplicationState((state) => state.noteDetails.noteTitle)
useDocumentTitle(noteTitle === '' ? untitledNote : noteTitle) useDocumentTitle(noteTitle === '' ? untitledNote : noteTitle)
} }

View file

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

View file

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