fix(frontend): improve performance by caching translated texts

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-06-26 22:32:19 +02:00
parent ced4cd953c
commit 76242330fd
81 changed files with 341 additions and 292 deletions

View file

@ -9,8 +9,7 @@ import { createNumberRangeArray } from '../../common/number-range/number-range'
import styles from './animations.module.scss'
import { IconRow } from './icon-row'
import React, { useMemo } from 'react'
import { Pencil as IconPencil } from 'react-bootstrap-icons'
import { PencilFill as IconPencilFill } from 'react-bootstrap-icons'
import { Pencil as IconPencil, PencilFill as IconPencilFill } from 'react-bootstrap-icons'
export interface HedgeDocLogoProps {
error: boolean

View file

@ -6,18 +6,20 @@
import styles from './animations.module.scss'
import React, { Fragment, useEffect, useState } from 'react'
import type { Icon } from 'react-bootstrap-icons'
import { FileText as IconFileText } from 'react-bootstrap-icons'
import { File as IconFile } from 'react-bootstrap-icons'
import { Fonts as IconFonts } from 'react-bootstrap-icons'
import { Gear as IconGear } from 'react-bootstrap-icons'
import { KeyboardFill as IconKeyboardFill } from 'react-bootstrap-icons'
import { ListCheck as IconListCheck } from 'react-bootstrap-icons'
import { Markdown as IconMarkdown } from 'react-bootstrap-icons'
import { Pencil as IconPencil } from 'react-bootstrap-icons'
import { Person as IconPerson } from 'react-bootstrap-icons'
import { Tag as IconTag } from 'react-bootstrap-icons'
import { TypeBold as IconTypeBold } from 'react-bootstrap-icons'
import { TypeItalic as IconTypeItalic } from 'react-bootstrap-icons'
import {
File as IconFile,
FileText as IconFileText,
Fonts as IconFonts,
Gear as IconGear,
KeyboardFill as IconKeyboardFill,
ListCheck as IconListCheck,
Markdown as IconMarkdown,
Pencil as IconPencil,
Person as IconPerson,
Tag as IconTag,
TypeBold as IconTypeBold,
TypeItalic as IconTypeItalic
} from 'react-bootstrap-icons'
const elements: Icon[] = [
IconFileText,

View file

@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../../hooks/common/use-translated-text'
import type { PropsWithDataCypressId } from '../../../../utils/cypress-attribute'
import { cypressId } from '../../../../utils/cypress-attribute'
import { UiIcon } from '../../icons/ui-icon'
@ -11,7 +12,6 @@ import React, { Fragment, useRef } from 'react'
import { Button } from 'react-bootstrap'
import { Files as IconFiles } from 'react-bootstrap-icons'
import type { Variant } from 'react-bootstrap/types'
import { useTranslation } from 'react-i18next'
export interface CopyToClipboardButtonProps extends PropsWithDataCypressId {
content: string
@ -33,10 +33,9 @@ export const CopyToClipboardButton: React.FC<CopyToClipboardButtonProps> = ({
variant = 'dark',
...props
}) => {
const { t } = useTranslation()
const button = useRef<HTMLButtonElement>(null)
const [copyToClipboard, overlayElement] = useCopyOverlay(button, content)
const buttonTitle = useTranslatedText('renderer.highlightCode.copyCode')
return (
<Fragment>
@ -44,7 +43,7 @@ export const CopyToClipboardButton: React.FC<CopyToClipboardButtonProps> = ({
ref={button}
size={size}
variant={variant}
title={t('renderer.highlightCode.copyCode') ?? undefined}
title={buttonTitle}
onClick={copyToClipboard}
{...cypressId(props)}>
<UiIcon icon={IconFiles} />

View file

@ -3,10 +3,11 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import type { CommonFieldProps } from './fields'
import React from 'react'
import { Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { Trans } from 'react-i18next'
/**
* Renders an input field for the current password when changing passwords.
@ -14,7 +15,7 @@ import { Trans, useTranslation } from 'react-i18next'
* @param onChange Hook that is called when the entered password changes.
*/
export const CurrentPasswordField: React.FC<CommonFieldProps> = ({ onChange }) => {
const { t } = useTranslation()
const placeholderText = useTranslatedText('login.auth.password')
return (
<Form.Group>
@ -25,7 +26,7 @@ export const CurrentPasswordField: React.FC<CommonFieldProps> = ({ onChange }) =
type='password'
size='sm'
onChange={onChange}
placeholder={t('login.auth.password') ?? undefined}
placeholder={placeholderText}
autoComplete='current-password'
required
/>

View file

@ -3,10 +3,11 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import type { CommonFieldProps } from './fields'
import React, { useMemo } from 'react'
import { Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { Trans } from 'react-i18next'
interface DisplayNameFieldProps extends CommonFieldProps {
initialValue?: string
@ -20,11 +21,8 @@ interface DisplayNameFieldProps extends CommonFieldProps {
* @param initialValue The initial input field value.
*/
export const DisplayNameField: React.FC<DisplayNameFieldProps> = ({ onChange, value, initialValue }) => {
const { t } = useTranslation()
const isValid = useMemo(() => {
return value.trim() !== '' && value !== initialValue
}, [value, initialValue])
const isValid = useMemo(() => value.trim() !== '' && value !== initialValue, [value, initialValue])
const placeholderText = useTranslatedText('profile.displayName')
return (
<Form.Group>
@ -37,7 +35,7 @@ export const DisplayNameField: React.FC<DisplayNameFieldProps> = ({ onChange, va
value={value}
isValid={isValid}
onChange={onChange}
placeholder={t('profile.displayName') ?? undefined}
placeholder={placeholderText}
autoComplete='name'
required
/>

View file

@ -3,10 +3,11 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import type { CommonFieldProps } from './fields'
import React, { useMemo } from 'react'
import { Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { Trans } from 'react-i18next'
/**
* Renders an input field for the new password when registering.
@ -15,11 +16,9 @@ import { Trans, useTranslation } from 'react-i18next'
* @param value The currently entered password.
*/
export const NewPasswordField: React.FC<CommonFieldProps> = ({ onChange, value, hasError = false }) => {
const { t } = useTranslation()
const isValid = useMemo(() => value.trim() !== '', [value])
const isValid = useMemo(() => {
return value.trim() !== ''
}, [value])
const placeholderText = useTranslatedText('login.auth.password')
return (
<Form.Group>
@ -32,7 +31,7 @@ export const NewPasswordField: React.FC<CommonFieldProps> = ({ onChange, value,
isValid={isValid}
isInvalid={hasError}
onChange={onChange}
placeholder={t('login.auth.password') ?? undefined}
placeholder={placeholderText}
autoComplete='new-password'
required
/>

View file

@ -3,10 +3,11 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import type { CommonFieldProps } from './fields'
import React, { useMemo } from 'react'
import { Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { Trans } from 'react-i18next'
interface PasswordAgainFieldProps extends CommonFieldProps {
password: string
@ -15,9 +16,10 @@ interface PasswordAgainFieldProps extends CommonFieldProps {
/**
* Renders an input field for typing the new password again when registering.
*
* @param onChange Hook that is called when the entered retype of the password changes.
* @param value The currently entered retype of the password.
* @param password The password entered into the password input field.
* @param onChange Hook that is called when the entered retype of the password changes
* @param value The currently entered retype of the password
* @param password The password entered into the password input field
* @param hasError Defines if the password should be shown as invalid
*/
export const PasswordAgainField: React.FC<PasswordAgainFieldProps> = ({
onChange,
@ -25,15 +27,9 @@ export const PasswordAgainField: React.FC<PasswordAgainFieldProps> = ({
password,
hasError = false
}) => {
const { t } = useTranslation()
const isInvalid = useMemo(() => {
return value !== '' && password !== value && hasError
}, [password, value, hasError])
const isValid = useMemo(() => {
return password !== '' && password === value && !hasError
}, [password, value, hasError])
const isInvalid = useMemo(() => value !== '' && password !== value && hasError, [password, value, hasError])
const isValid = useMemo(() => password !== '' && password === value && !hasError, [password, value, hasError])
const placeholderText = useTranslatedText('login.register.passwordAgain')
return (
<Form.Group>
@ -46,7 +42,7 @@ export const PasswordAgainField: React.FC<PasswordAgainFieldProps> = ({
isInvalid={isInvalid}
isValid={isValid}
onChange={onChange}
placeholder={t('login.register.passwordAgain') ?? undefined}
placeholder={placeholderText}
autoComplete='new-password'
required
/>

View file

@ -3,10 +3,10 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import type { CommonFieldProps } from './fields'
import React from 'react'
import { Form } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
export interface UsernameFieldProps extends CommonFieldProps {
isInvalid?: boolean
@ -22,7 +22,7 @@ export interface UsernameFieldProps extends CommonFieldProps {
* @param isInvalid Adds error style to label
*/
export const UsernameField: React.FC<UsernameFieldProps> = ({ onChange, value, isValid, isInvalid }) => {
const { t } = useTranslation()
const placeholderText = useTranslatedText('login.auth.username')
return (
<Form.Control
@ -32,7 +32,7 @@ export const UsernameField: React.FC<UsernameFieldProps> = ({ onChange, value, i
isValid={isValid}
isInvalid={isInvalid}
onChange={onChange}
placeholder={t('login.auth.username') ?? undefined}
placeholder={placeholderText}
autoComplete='username'
autoFocus={true}
required

View file

@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { UsernameField } from './username-field'
import type { UsernameFieldProps } from './username-field'
import { UsernameField } from './username-field'
import React from 'react'
import { Form } from 'react-bootstrap'
import { Trans } from 'react-i18next'

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { testId } from '../../../utils/test-id'
import { BootstrapLazyIcons } from './bootstrap-icons'
import type { BootstrapIconName } from './bootstrap-icons'
import { BootstrapLazyIcons } from './bootstrap-icons'
import React, { Suspense, useMemo } from 'react'
export interface LazyBootstrapIconProps {

View file

@ -3,10 +3,10 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import { ExternalLink } from './external-link'
import type { TranslatedLinkProps } from './types'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import React from 'react'
/**
* An {@link ExternalLink external link} with translated text.
@ -16,7 +16,6 @@ import { useTranslation } from 'react-i18next'
* @param props Additional props directly given to the {@link ExternalLink}
*/
export const TranslatedExternalLink: React.FC<TranslatedLinkProps> = ({ i18nKey, i18nOption, ...props }) => {
const { t } = useTranslation()
const text = useMemo(() => (i18nOption ? t(i18nKey, i18nOption) : t(i18nKey)), [i18nKey, i18nOption, t])
const text = useTranslatedText(i18nKey, i18nOption)
return <ExternalLink text={text} {...props} />
}

View file

@ -3,10 +3,10 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import { InternalLink } from './internal-link'
import type { TranslatedLinkProps } from './types'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import React from 'react'
/**
* An {@link InternalLink internal link} with translated text.
@ -16,7 +16,7 @@ import { useTranslation } from 'react-i18next'
* @param props Additional props directly given to the {@link InternalLink}
*/
export const TranslatedInternalLink: React.FC<TranslatedLinkProps> = ({ i18nKey, i18nOption, ...props }) => {
const { t } = useTranslation()
const text = useMemo(() => (i18nOption ? t(i18nKey, i18nOption) : t(i18nKey)), [i18nKey, i18nOption, t])
const text = useTranslatedText(i18nKey, i18nOption)
return <InternalLink text={text} {...props} />
}

View file

@ -11,7 +11,7 @@ import { RendererType } from '../../render-page/window-post-message-communicator
import { CommonModal } from '../modals/common-modal'
import { RendererIframe } from '../renderer-iframe/renderer-iframe'
import { fetchMotd, MOTD_LOCAL_STORAGE_KEY } from './fetch-motd'
import React, { useCallback, useMemo, useEffect, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Button, Modal } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useAsync } from 'react-use'

View file

@ -10,9 +10,11 @@ import { UiIcon } from '../icons/ui-icon'
import { ShowIf } from '../show-if/show-if'
import React, { useCallback, useEffect } from 'react'
import { Alert, Button } from 'react-bootstrap'
import { ArrowRepeat as IconArrowRepeat } from 'react-bootstrap-icons'
import { CheckCircle as IconCheckCircle } from 'react-bootstrap-icons'
import { ExclamationTriangle as IconExclamationTriangle } from 'react-bootstrap-icons'
import {
ArrowRepeat as IconArrowRepeat,
CheckCircle as IconCheckCircle,
ExclamationTriangle as IconExclamationTriangle
} from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
import { useAsyncFn } from 'react-use'

View file

@ -15,9 +15,9 @@ import { useEditorReceiveHandler } from '../../render-page/window-post-message-c
import type {
ExtensionEvent,
OnHeightChangeMessage,
RendererType,
SetScrollStateMessage
} from '../../render-page/window-post-message-communicator/rendering-message'
import type { RendererType } from '../../render-page/window-post-message-communicator/rendering-message'
import { CommunicationMessageType } from '../../render-page/window-post-message-communicator/rendering-message'
import { ShowIf } from '../show-if/show-if'
import { WaitSpinner } from '../wait-spinner/wait-spinner'

View file

@ -3,13 +3,13 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import { ShowIf } from '../show-if/show-if'
import defaultAvatar from './default-avatar.png'
import styles from './user-avatar.module.scss'
import React, { useCallback, useMemo } from 'react'
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
import type { OverlayInjectedProps } from 'react-bootstrap/Overlay'
import { useTranslation } from 'react-i18next'
export interface UserAvatarProps {
size?: 'sm' | 'lg'
@ -34,8 +34,6 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({
additionalClasses = '',
showName = true
}) => {
const { t } = useTranslation()
const imageSize = useMemo(() => {
switch (size) {
case 'sm':
@ -51,7 +49,13 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({
return photoUrl || defaultAvatar.src
}, [photoUrl])
const imgDescription = useMemo(() => t('common.avatarOf', { name: displayName }), [t, displayName])
const imageTranslateOptions = useMemo(
() => ({
name: displayName
}),
[displayName]
)
const imgDescription = useTranslatedText('common.avatarOf', imageTranslateOptions)
const tooltip = useCallback(
(overlayInjectedProps: OverlayInjectedProps) => (

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../hooks/common/use-application-state'
import { useTranslatedText } from '../../hooks/common/use-translated-text'
import { InternalLink } from '../common/links/internal-link'
import { ShowIf } from '../common/show-if/show-if'
import { NoteInfoLineCreatedAt } from '../editor-page/sidebar/specific-sidebar-entries/note-info-sidebar-menu/note-info-line/note-info-line-created-at'
@ -11,16 +12,16 @@ import { NoteInfoLineUpdatedBy } from '../editor-page/sidebar/specific-sidebar-e
import styles from './document-infobar.module.scss'
import React from 'react'
import { Pencil as IconPencil } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
import { Trans } from 'react-i18next'
/**
* Renders an info bar with metadata about the current note.
*/
export const DocumentInfobar: React.FC = () => {
const { t } = useTranslation()
const noteDetails = useApplicationState((state) => state.noteDetails)
// TODO Check permissions ("writability") of note and show edit link depending on that.
const linkTitle = useTranslatedText('views.readOnly.editNote')
return (
<div className={`d-flex flex-row my-3 ${styles['document-infobar']}`}>
<div className={'col-md'}>&nbsp;</div>
@ -38,7 +39,7 @@ export const DocumentInfobar: React.FC = () => {
href={`/n/${noteDetails.primaryAddress}`}
icon={IconPencil}
className={'text-primary text-decoration-none mx-1'}
title={t('views.readOnly.editNote') ?? undefined}
title={linkTitle}
/>
</ShowIf>
</span>

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
import { useTranslatedText } from '../../../../hooks/common/use-translated-text'
import { useOutlineButtonVariant } from '../../../../hooks/dark-mode/use-outline-button-variant'
import { cypressId } from '../../../../utils/cypress-attribute'
import { CommonModal } from '../../../common/modals/common-modal'
@ -11,21 +12,21 @@ import { CheatsheetContent } from './cheatsheet-content'
import { CheatsheetInNewTabButton } from './cheatsheet-in-new-tab-button'
import React, { Fragment } from 'react'
import { Button, Modal } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { Trans } from 'react-i18next'
/**
* Shows a button that opens the cheatsheet dialog.
*/
export const CheatsheetButton: React.FC = () => {
const { t } = useTranslation()
const [modalVisibility, showModal, closeModal] = useBooleanState()
const buttonVariant = useOutlineButtonVariant()
const buttonTitle = useTranslatedText('cheatsheet.button')
return (
<Fragment>
<Button
{...cypressId('open.cheatsheet-button')}
title={t('cheatsheet.button') ?? undefined}
title={buttonTitle}
className={'mx-2'}
variant={buttonVariant}
size={'sm'}

View file

@ -3,12 +3,12 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../../hooks/common/use-translated-text'
import { useOutlineButtonVariant } from '../../../../hooks/dark-mode/use-outline-button-variant'
import { IconButton } from '../../../common/icon-button/icon-button'
import type { MouseEvent } from 'react'
import React, { useCallback } from 'react'
import { BoxArrowUpRight } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
export interface CheatsheetInNewTabButtonProps {
onClick?: () => void
@ -26,10 +26,8 @@ export const CheatsheetInNewTabButton: React.FC<CheatsheetInNewTabButtonProps> =
},
[onClick]
)
const { t } = useTranslation()
const buttonVariant = useOutlineButtonVariant()
const buttonTitle = useTranslatedText('cheatsheet.modal.popup')
return (
<IconButton
@ -41,7 +39,7 @@ export const CheatsheetInNewTabButton: React.FC<CheatsheetInNewTabButtonProps> =
className={'p-2 border-0'}
variant={buttonVariant}
target={'_blank'}
title={t('cheatsheet.modal.popup') ?? ''}
title={buttonTitle}
/>
)
}

View file

@ -4,28 +4,29 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
import { useTranslatedText } from '../../../../hooks/common/use-translated-text'
import { useOutlineButtonVariant } from '../../../../hooks/dark-mode/use-outline-button-variant'
import { cypressId } from '../../../../utils/cypress-attribute'
import { IconButton } from '../../../common/icon-button/icon-button'
import { HelpModal } from './help-modal'
import React, { Fragment } from 'react'
import { QuestionCircle as IconQuestionCircle } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
import { Trans } from 'react-i18next'
/**
* Renders the button to open the {@link HelpModal}.
*/
export const HelpButton: React.FC = () => {
const { t } = useTranslation()
const [modalVisibility, showModal, closeModal] = useBooleanState()
const buttonVariant = useOutlineButtonVariant()
const buttonTitle = useTranslatedText('editor.documentBar.help')
return (
<Fragment>
<IconButton
icon={IconQuestionCircle}
{...cypressId('editor-help-button')}
title={t('editor.documentBar.help') ?? undefined}
title={buttonTitle}
className='ms-2'
size='sm'
variant={buttonVariant}

View file

@ -9,10 +9,7 @@ import { TranslatedExternalLink } from '../../../common/links/translated-externa
import { TranslatedInternalLink } from '../../../common/links/translated-internal-link'
import React from 'react'
import { Col, Row } from 'react-bootstrap'
import { Dot as IconDot } from 'react-bootstrap-icons'
import { Flag as IconFlag } from 'react-bootstrap-icons'
import { PeopleFill as IconPeopleFill } from 'react-bootstrap-icons'
import { Tag as IconTag } from 'react-bootstrap-icons'
import { Dot as IconDot, Flag as IconFlag, PeopleFill as IconPeopleFill, Tag as IconTag } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
/**

View file

@ -4,25 +4,25 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import { useOutlineButtonVariant } from '../../../hooks/dark-mode/use-outline-button-variant'
import { UiIcon } from '../../common/icons/ui-icon'
import Link from 'next/link'
import React from 'react'
import { Button } from 'react-bootstrap'
import { FileEarmarkTextFill as IconFileEarmarkTextFill } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
/**
* Button that links to the read-only version of a note.
*/
export const ReadOnlyModeButton: React.FC = () => {
const { t } = useTranslation()
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
const buttonVariant = useOutlineButtonVariant()
const buttonTitle = useTranslatedText('editor.documentBar.readOnlyMode')
return (
<Link href={`/s/${noteIdentifier}`} target='_blank'>
<Button title={t('editor.documentBar.readOnlyMode') ?? undefined} size='sm' variant={buttonVariant}>
<Button title={buttonTitle} size='sm' variant={buttonVariant}>
<UiIcon icon={IconFileEarmarkTextFill} />
</Button>
</Link>

View file

@ -4,25 +4,25 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import { useOutlineButtonVariant } from '../../../hooks/dark-mode/use-outline-button-variant'
import { UiIcon } from '../../common/icons/ui-icon'
import Link from 'next/link'
import React from 'react'
import { Button } from 'react-bootstrap'
import { Tv as IconTv } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
/**
* Button that links to the slide-show presentation of the current note.
*/
export const SlideModeButton: React.FC = () => {
const { t } = useTranslation()
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
const buttonVariant = useOutlineButtonVariant()
const buttonTitle = useTranslatedText('editor.documentBar.slideMode')
return (
<Link href={`/p/${noteIdentifier}`} target='_blank'>
<Button title={t('editor.documentBar.slideMode') ?? undefined} className='ms-2' size='sm' variant={buttonVariant}>
<Button title={buttonTitle} className='ms-2' size='sm' variant={buttonVariant}>
<UiIcon icon={IconTv} />
</Button>
</Link>

View file

@ -5,10 +5,8 @@
*/
import type { ChangeSpec, Transaction } from '@codemirror/state'
import { Annotation } from '@codemirror/state'
import type { EditorView, PluginValue } from '@codemirror/view'
import type { ViewUpdate } from '@codemirror/view'
import type { Text as YText } from 'yjs'
import type { Transaction as YTransaction, YTextEvent } from 'yjs'
import type { EditorView, PluginValue, ViewUpdate } from '@codemirror/view'
import type { Text as YText, Transaction as YTransaction, YTextEvent } from 'yjs'
const syncAnnotation = Annotation.define()

View file

@ -7,7 +7,7 @@ import { concatCssClasses } from '../../../../../utils/concat-css-classes'
import { createCursorCssClass } from './create-cursor-css-class'
import styles from './style.module.scss'
import type { SelectionRange } from '@codemirror/state'
import type { LayerMarker, EditorView, Rect } from '@codemirror/view'
import type { EditorView, LayerMarker, Rect } from '@codemirror/view'
import { Direction } from '@codemirror/view'
/**

View file

@ -6,6 +6,7 @@
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { ORIGIN, useBaseUrl } from '../../../hooks/common/use-base-url'
import { useMayEdit } from '../../../hooks/common/use-may-edit'
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import { useDarkModeState } from '../../../hooks/dark-mode/use-dark-mode-state'
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
import { findLanguageByCodeBlockName } from '../../markdown-renderer/extensions/_base-classes/code-block-markdown-extension/find-language-by-code-block-name'
@ -42,7 +43,6 @@ import { lintGutter } from '@codemirror/lint'
import { oneDark } from '@codemirror/theme-one-dark'
import ReactCodeMirror from '@uiw/react-codemirror'
import React, { useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
export type EditorPaneProps = ScrollProps
@ -129,7 +129,6 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ scrollState, onScroll, o
[ligaturesEnabled]
)
const { t } = useTranslation()
const darkModeActivated = useDarkModeState()
const editorOrigin = useBaseUrl(ORIGIN.EDITOR)
const isSynced = useApplicationState((state) => state.realtimeStatus.isSynced)
@ -142,6 +141,9 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ scrollState, onScroll, o
}
}, [messageTransporter])
const translateOptions = useMemo(() => ({ host: editorOrigin }), [editorOrigin])
const placeholderText = useTranslatedText('editor.placeholder', translateOptions)
return (
<div
className={`d-flex flex-column h-100 position-relative`}
@ -153,7 +155,7 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ scrollState, onScroll, o
<ToolBar />
<ReactCodeMirror
editable={updateViewContextExtension !== null && isSynced && mayEdit}
placeholder={t('editor.placeholder', { host: editorOrigin }) ?? ''}
placeholder={placeholderText}
extensions={extensions}
width={'100%'}
height={'100%'}

View file

@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ConnectionState } from '@hedgedoc/commons'
import type { TransportAdapter } from '@hedgedoc/commons'
import { ConnectionState } from '@hedgedoc/commons'
import type { Message, MessageType } from '@hedgedoc/commons/dist'
/**

View file

@ -7,8 +7,7 @@ import { useApplicationState } from '../../../../../hooks/common/use-application
import { YTextSyncViewPlugin } from '../../codemirror-extensions/document-sync/y-text-sync-view-plugin'
import type { Extension } from '@codemirror/state'
import { ViewPlugin } from '@codemirror/view'
import type { YDocSyncClientAdapter } from '@hedgedoc/commons'
import type { RealtimeDoc } from '@hedgedoc/commons'
import type { RealtimeDoc, YDocSyncClientAdapter } from '@hedgedoc/commons'
import { useEffect, useMemo, useState } from 'react'
/**

View file

@ -7,8 +7,7 @@ import fontStyles from '../../../../../../global-styles/variables.module.scss'
import { useDarkModeState } from '../../../../../hooks/dark-mode/use-dark-mode-state'
import styles from './emoji-picker.module.scss'
import { Picker } from 'emoji-picker-element'
import type { EmojiClickEvent, EmojiClickEventDetail } from 'emoji-picker-element/shared'
import type { PickerConstructorOptions } from 'emoji-picker-element/shared'
import type { EmojiClickEvent, EmojiClickEventDetail, PickerConstructorOptions } from 'emoji-picker-element/shared'
import React, { useEffect, useRef } from 'react'
import { Popover } from 'react-bootstrap'
import type { PopoverProps } from 'react-bootstrap/Popover'

View file

@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../../../hooks/common/use-translated-text'
import { cypressId } from '../../../../../utils/cypress-attribute'
import { UiIcon } from '../../../../common/icons/ui-icon'
import { CommonModal } from '../../../../common/modals/common-modal'
@ -10,9 +11,8 @@ import type { TableSize } from './table-size-picker-popover'
import type { ChangeEvent } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import { Button, Form, ModalFooter } from 'react-bootstrap'
import { Table as IconTable } from 'react-bootstrap-icons'
import { X as IconX } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
import { Table as IconTable, X as IconX } from 'react-bootstrap-icons'
import { Trans } from 'react-i18next'
export interface CustomTableSizeModalProps {
showModal: boolean
@ -33,7 +33,6 @@ const initialTableSize: TableSize = {
* @param onSizeSelect is called if the user entered and confirmed a custom table size
*/
export const CustomTableSizeModal: React.FC<CustomTableSizeModalProps> = ({ showModal, onDismiss, onSizeSelect }) => {
const { t } = useTranslation()
const [tableSize, setTableSize] = useState<TableSize>(() => initialTableSize)
useEffect(() => {
@ -63,6 +62,9 @@ export const CustomTableSizeModal: React.FC<CustomTableSizeModalProps> = ({ show
}))
}, [])
const columnPlaceholderText = useTranslatedText('editor.editorToolbar.table.cols')
const rowsPlaceholderText = useTranslatedText('editor.editorToolbar.table.rows')
return (
<CommonModal
show={showModal}
@ -75,7 +77,7 @@ export const CustomTableSizeModal: React.FC<CustomTableSizeModalProps> = ({ show
<Form.Control
type={'number'}
min={1}
placeholder={t('editor.editorToolbar.table.cols') ?? undefined}
placeholder={columnPlaceholderText}
isInvalid={tableSize.columns <= 0}
onChange={onColChange}
/>
@ -83,7 +85,7 @@ export const CustomTableSizeModal: React.FC<CustomTableSizeModalProps> = ({ show
<Form.Control
type={'number'}
min={1}
placeholder={t('editor.editorToolbar.table.rows') ?? undefined}
placeholder={rowsPlaceholderText}
isInvalid={tableSize.rows <= 0}
onChange={onRowChange}
/>

View file

@ -61,7 +61,7 @@ export const TableSizePickerPopover = React.forwardRef<HTMLDivElement, TableSize
{...cypressAttribute('col', `${col + 1}`)}
{...cypressAttribute('row', `${row + 1}`)}
onMouseEnter={onSizeHover(row + 1, col + 1)}
title={t('editor.editorToolbar.table.titleWithSize', { cols: col + 1, rows: row + 1 }) ?? undefined}
title={t('editor.editorToolbar.table.titleWithSize', { cols: col + 1, rows: row + 1 })}
onClick={() => onTableSizeSelected(row + 1, col + 1)}
/>
)

View file

@ -4,8 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { getGlobalState } from '../../../../redux'
import type { ScrollState } from '../../synced-scroll/scroll-props'
import type { ScrollCallback } from '../../synced-scroll/scroll-props'
import type { ScrollCallback, ScrollState } from '../../synced-scroll/scroll-props'
import { useMemo } from 'react'
/**

View file

@ -10,7 +10,7 @@ import { mockI18n } from '../../../../../../test-utils/mock-i18n'
import { mockNoteOwnership } from '../../../../../../test-utils/note-ownership'
import * as useUiNotificationsModule from '../../../../../notifications/ui-notification-boundary'
import { AliasesAddForm } from './aliases-add-form'
import { render, act, screen } from '@testing-library/react'
import { act, render, screen } from '@testing-library/react'
import testEvent from '@testing-library/user-event'
import React from 'react'

View file

@ -7,6 +7,7 @@ import { addAlias } from '../../../../../../api/alias'
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
import { useIsOwner } from '../../../../../../hooks/common/use-is-owner'
import { useOnInputChange } from '../../../../../../hooks/common/use-on-input-change'
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
import { updateMetadata } from '../../../../../../redux/note-details/methods'
import { testId } from '../../../../../../utils/test-id'
import { UiIcon } from '../../../../../common/icons/ui-icon'
@ -15,7 +16,6 @@ import type { FormEvent } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import { Button, Form, InputGroup } from 'react-bootstrap'
import { Plus as IconPlus } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
const validAliasRegex = /^[a-z0-9_-]*$/
@ -23,7 +23,6 @@ const validAliasRegex = /^[a-z0-9_-]*$/
* Form for adding a new alias to a note.
*/
export const AliasesAddForm: React.FC = () => {
const { t } = useTranslation()
const { showErrorNotification } = useUiNotifications()
const noteId = useApplicationState((state) => state.noteDetails.id)
const isOwner = useIsOwner()
@ -48,12 +47,14 @@ export const AliasesAddForm: React.FC = () => {
return validAliasRegex.test(newAlias)
}, [newAlias])
const addAliasText = useTranslatedText('editor.modal.aliases.addAlias')
return (
<form onSubmit={onAddAlias}>
<InputGroup className={'me-1 mb-1'} hasValidation={true}>
<Form.Control
value={newAlias}
placeholder={t('editor.modal.aliases.addAlias') ?? undefined}
placeholder={addAliasText}
onChange={onNewAliasInputChange}
isInvalid={!newAliasValid}
disabled={!isOwner}
@ -65,7 +66,7 @@ export const AliasesAddForm: React.FC = () => {
variant='light'
className={'text-secondary ms-2'}
disabled={!isOwner || !newAliasValid || newAlias === ''}
title={t('editor.modal.aliases.addAlias') ?? undefined}
title={addAliasText}
{...testId('addAliasButton')}>
<UiIcon icon={IconPlus} />
</Button>

View file

@ -10,7 +10,7 @@ import { mockI18n } from '../../../../../../test-utils/mock-i18n'
import { mockNoteOwnership } from '../../../../../../test-utils/note-ownership'
import * as useUiNotificationsModule from '../../../../../notifications/ui-notification-boundary'
import { AliasesListEntry } from './aliases-list-entry'
import { render, act, screen } from '@testing-library/react'
import { act, render, screen } from '@testing-library/react'
import React from 'react'
jest.mock('../../../../../../api/alias')

View file

@ -6,6 +6,7 @@
import { deleteAlias, markAliasAsPrimary } from '../../../../../../api/alias'
import type { Alias } from '../../../../../../api/alias/types'
import { useIsOwner } from '../../../../../../hooks/common/use-is-owner'
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
import { updateMetadata } from '../../../../../../redux/note-details/methods'
import { testId } from '../../../../../../utils/test-id'
import { UiIcon } from '../../../../../common/icons/ui-icon'
@ -13,9 +14,7 @@ import { ShowIf } from '../../../../../common/show-if/show-if'
import { useUiNotifications } from '../../../../../notifications/ui-notification-boundary'
import React, { useCallback } from 'react'
import { Button } from 'react-bootstrap'
import { StarFill as IconStarFill } from 'react-bootstrap-icons'
import { Star as IconStar } from 'react-bootstrap-icons'
import { X as IconX } from 'react-bootstrap-icons'
import { Star as IconStar, StarFill as IconStarFill, X as IconX } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
export interface AliasesListEntryProps {
@ -44,6 +43,10 @@ export const AliasesListEntry: React.FC<AliasesListEntryProps> = ({ alias }) =>
.catch(showErrorNotification(t('editor.modal.aliases.errorMakingPrimary')))
}, [alias, t, showErrorNotification])
const isPrimaryText = useTranslatedText('editor.modal.aliases.isPrimary')
const makePrimaryText = useTranslatedText('editor.modal.aliases.makePrimary')
const removeAliasText = useTranslatedText('editor.modal.aliases.removeAlias')
return (
<li className={'list-group-item d-flex flex-row justify-content-between align-items-center'}>
{alias.name}
@ -53,7 +56,7 @@ export const AliasesListEntry: React.FC<AliasesListEntryProps> = ({ alias }) =>
className={'me-2 text-warning'}
variant='light'
disabled={true}
title={t('editor.modal.aliases.isPrimary') ?? undefined}
title={isPrimaryText}
{...testId('aliasIsPrimary')}>
<UiIcon icon={IconStar} />
</Button>
@ -63,7 +66,7 @@ export const AliasesListEntry: React.FC<AliasesListEntryProps> = ({ alias }) =>
className={'me-2'}
variant='light'
disabled={!isOwner}
title={t('editor.modal.aliases.makePrimary') ?? undefined}
title={makePrimaryText}
onClick={onMakePrimaryClick}
{...testId('aliasButtonMakePrimary')}>
<UiIcon icon={IconStarFill} />
@ -73,7 +76,7 @@ export const AliasesListEntry: React.FC<AliasesListEntryProps> = ({ alias }) =>
variant='light'
className={'text-danger'}
disabled={!isOwner}
title={t('editor.modal.aliases.removeAlias') ?? undefined}
title={removeAliasText}
onClick={onRemoveClick}
{...testId('aliasButtonRemove')}>
<UiIcon icon={IconX} />

View file

@ -7,8 +7,8 @@ import type { Alias } from '../../../../../../api/alias/types'
import * as useApplicationStateModule from '../../../../../../hooks/common/use-application-state'
import { mockI18n } from '../../../../../../test-utils/mock-i18n'
import { AliasesList } from './aliases-list'
import * as AliasesListEntryModule from './aliases-list-entry'
import type { AliasesListEntryProps } from './aliases-list-entry'
import * as AliasesListEntryModule from './aliases-list-entry'
import { render } from '@testing-library/react'
import React from 'react'

View file

@ -11,8 +11,8 @@ import * as AliasesAddFormModule from './aliases-add-form'
import * as AliasesListModule from './aliases-list'
import { AliasesModal } from './aliases-modal'
import { render } from '@testing-library/react'
import React from 'react'
import type { PropsWithChildren } from 'react'
import React from 'react'
jest.mock('./aliases-list')
jest.mock('./aliases-add-form')

View file

@ -11,10 +11,12 @@ import type { SpecificSidebarMenuProps } from '../types'
import { DocumentSidebarMenuSelection } from '../types'
import { ExportMarkdownSidebarEntry } from './export-markdown-sidebar-entry'
import React, { Fragment, useCallback } from 'react'
import { ArrowLeft as IconArrowLeft } from 'react-bootstrap-icons'
import { CloudDownload as IconCloudDownload } from 'react-bootstrap-icons'
import { FileCode as IconFileCode } from 'react-bootstrap-icons'
import { Github as IconGithub } from 'react-bootstrap-icons'
import {
ArrowLeft as IconArrowLeft,
CloudDownload as IconCloudDownload,
FileCode as IconFileCode,
Github as IconGithub
} from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
/**

View file

@ -11,10 +11,12 @@ import type { SpecificSidebarMenuProps } from '../types'
import { DocumentSidebarMenuSelection } from '../types'
import { ImportMarkdownSidebarEntry } from './import-markdown-sidebar-entry'
import React, { Fragment, useCallback } from 'react'
import { ArrowLeft as IconArrowLeft } from 'react-bootstrap-icons'
import { Clipboard as IconClipboard } from 'react-bootstrap-icons'
import { CloudUpload as IconCloudUpload } from 'react-bootstrap-icons'
import { Github as IconGithub } from 'react-bootstrap-icons'
import {
ArrowLeft as IconArrowLeft,
Clipboard as IconClipboard,
CloudUpload as IconCloudUpload,
Github as IconGithub
} from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
/**

View file

@ -4,12 +4,12 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useLowercaseOnInputChange } from '../../../../../../hooks/common/use-lowercase-on-input-change'
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
import { UiIcon } from '../../../../../common/icons/ui-icon'
import type { PermissionDisabledProps } from './permission-disabled.prop'
import React, { useCallback, useState } from 'react'
import { Button, FormControl, InputGroup } from 'react-bootstrap'
import { Plus as IconPlus } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
export interface PermissionAddEntryFieldProps {
onAddEntry: (identifier: string) => void
@ -28,8 +28,6 @@ export const PermissionAddEntryField: React.FC<PermissionAddEntryFieldProps & Pe
i18nKey,
disabled
}) => {
const { t } = useTranslation()
const [newEntryIdentifier, setNewEntryIdentifier] = useState('')
const onChange = useLowercaseOnInputChange(setNewEntryIdentifier)
@ -37,19 +35,16 @@ export const PermissionAddEntryField: React.FC<PermissionAddEntryFieldProps & Pe
onAddEntry(newEntryIdentifier)
}, [newEntryIdentifier, onAddEntry])
const placeholderText = useTranslatedText(i18nKey)
return (
<li className={'list-group-item'}>
<InputGroup className={'me-1 mb-1'}>
<FormControl
value={newEntryIdentifier}
placeholder={t(i18nKey) ?? undefined}
onChange={onChange}
disabled={disabled}
/>
<FormControl value={newEntryIdentifier} placeholder={placeholderText} onChange={onChange} disabled={disabled} />
<Button
variant='light'
className={'text-secondary ms-2'}
title={t(i18nKey) ?? undefined}
title={placeholderText}
onClick={onSubmit}
disabled={disabled}>
<UiIcon icon={IconPlus} />

View file

@ -3,15 +3,13 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
import { UiIcon } from '../../../../../common/icons/ui-icon'
import type { PermissionDisabledProps } from './permission-disabled.prop'
import { AccessLevel } from '@hedgedoc/commons'
import React, { useMemo } from 'react'
import { Button, ToggleButtonGroup } from 'react-bootstrap'
import { Eye as IconEye } from 'react-bootstrap-icons'
import { Pencil as IconPencil } from 'react-bootstrap-icons'
import { X as IconX } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
import { Eye as IconEye, Pencil as IconPencil, X as IconX } from 'react-bootstrap-icons'
interface PermissionEntryButtonI18nKeys {
remove: string
@ -53,8 +51,6 @@ export const PermissionEntryButtons: React.FC<PermissionEntryButtonsProps & Perm
onRemove,
disabled
}) => {
const { t } = useTranslation()
const i18nKeys: PermissionEntryButtonI18nKeys = useMemo(() => {
switch (type) {
case PermissionType.USER:
@ -72,27 +68,27 @@ export const PermissionEntryButtons: React.FC<PermissionEntryButtonsProps & Perm
}
}, [type])
const translateOptions = useMemo(() => ({ name }), [name])
const removeTitle = useTranslatedText(i18nKeys.remove, translateOptions)
const setReadOnlyTitle = useTranslatedText(i18nKeys.setReadOnly, translateOptions)
const setWritableTitle = useTranslatedText(i18nKeys.setWriteable, translateOptions)
return (
<div>
<Button
variant='light'
className={'text-danger me-2'}
disabled={disabled}
title={t(i18nKeys.remove, { name }) ?? undefined}
onClick={onRemove}>
<Button variant='light' className={'text-danger me-2'} disabled={disabled} title={removeTitle} onClick={onRemove}>
<UiIcon icon={IconX} />
</Button>
<ToggleButtonGroup type='radio' name='edit-mode' value={currentSetting}>
<Button
disabled={disabled}
title={t(i18nKeys.setReadOnly, { name }) ?? undefined}
title={setReadOnlyTitle}
variant={currentSetting === AccessLevel.READ_ONLY ? 'secondary' : 'outline-secondary'}
onClick={onSetReadOnly}>
<UiIcon icon={IconEye} />
</Button>
<Button
disabled={disabled}
title={t(i18nKeys.setWriteable, { name }) ?? undefined}
title={setWritableTitle}
variant={currentSetting === AccessLevel.WRITEABLE ? 'secondary' : 'outline-secondary'}
onClick={onSetWriteable}>
<UiIcon icon={IconPencil} />

View file

@ -5,6 +5,7 @@
*/
import { removeGroupPermission, setGroupPermission } from '../../../../../../api/permissions'
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
import { setNotePermissionsFromServer } from '../../../../../../redux/note-details/methods'
import { IconButton } from '../../../../../common/icon-button/icon-button'
import { useUiNotifications } from '../../../../../notifications/ui-notification-boundary'
@ -12,9 +13,7 @@ import type { PermissionDisabledProps } from './permission-disabled.prop'
import { AccessLevel, SpecialGroup } from '@hedgedoc/commons'
import React, { useCallback, useMemo } from 'react'
import { ToggleButtonGroup } from 'react-bootstrap'
import { Eye as IconEye } from 'react-bootstrap-icons'
import { Pencil as IconPencil } from 'react-bootstrap-icons'
import { SlashCircle as IconSlashCircle } from 'react-bootstrap-icons'
import { Eye as IconEye, Pencil as IconPencil, SlashCircle as IconSlashCircle } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
export interface PermissionEntrySpecialGroupProps {
@ -71,6 +70,11 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
}
}, [type, t])
const translateOptions = useMemo(() => ({ name }), [name])
const denyGroupText = useTranslatedText('editor.modal.permissions.denyGroup', translateOptions)
const viewOnlyGroupText = useTranslatedText('editor.modal.permissions.viewOnlyGroup', translateOptions)
const editGroupText = useTranslatedText('editor.modal.permissions.editGroup', translateOptions)
return (
<li className={'list-group-item d-flex flex-row justify-content-between align-items-center'}>
<span>{name}</span>
@ -78,7 +82,7 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
<ToggleButtonGroup type='radio' name='edit-mode'>
<IconButton
icon={IconSlashCircle}
title={t('editor.modal.permissions.denyGroup', { name }) ?? undefined}
title={denyGroupText}
variant={level === AccessLevel.NONE ? 'secondary' : 'outline-secondary'}
onClick={onSetEntryDenied}
disabled={disabled}
@ -86,7 +90,7 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
/>
<IconButton
icon={IconEye}
title={t('editor.modal.permissions.viewOnlyGroup', { name }) ?? undefined}
title={viewOnlyGroupText}
variant={level === AccessLevel.READ_ONLY ? 'secondary' : 'outline-secondary'}
onClick={onSetEntryReadOnly}
disabled={disabled}
@ -94,7 +98,7 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
/>
<IconButton
icon={IconPencil}
title={t('editor.modal.permissions.editGroup', { name }) ?? undefined}
title={editGroupText}
variant={level === AccessLevel.WRITEABLE ? 'secondary' : 'outline-secondary'}
onClick={onSetEntryWriteable}
disabled={disabled}

View file

@ -4,11 +4,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useOnInputChange } from '../../../../../../hooks/common/use-on-input-change'
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
import { UiIcon } from '../../../../../common/icons/ui-icon'
import React, { useCallback, useMemo, useState } from 'react'
import { Button, FormControl, InputGroup } from 'react-bootstrap'
import { Check as IconCheck } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
export interface PermissionOwnerChangeProps {
onConfirmOwnerChange: (newOwner: string) => void
@ -20,7 +20,6 @@ export interface PermissionOwnerChangeProps {
* @param onConfirmOwnerChange The callback to call if the owner was changed.
*/
export const PermissionOwnerChange: React.FC<PermissionOwnerChangeProps> = ({ onConfirmOwnerChange }) => {
const { t } = useTranslation()
const [ownerFieldValue, setOwnerFieldValue] = useState('')
const onChangeField = useOnInputChange(setOwnerFieldValue)
@ -32,16 +31,15 @@ export const PermissionOwnerChange: React.FC<PermissionOwnerChangeProps> = ({ on
return ownerFieldValue.trim() === ''
}, [ownerFieldValue])
const placeholderText = useTranslatedText('editor.modal.permissions.ownerChange.placeholder')
const buttonTitleText = useTranslatedText('common.save')
return (
<InputGroup className={'me-1 mb-1'}>
<FormControl
value={ownerFieldValue}
placeholder={t('editor.modal.permissions.ownerChange.placeholder') ?? undefined}
onChange={onChangeField}
/>
<FormControl value={ownerFieldValue} placeholder={placeholderText} onChange={onChangeField} />
<Button
variant='light'
title={t('common.save') ?? undefined}
title={buttonTitleText}
onClick={onClickConfirm}
className={'ms-2'}
disabled={confirmButtonDisabled}>

View file

@ -4,13 +4,13 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
import { UiIcon } from '../../../../../common/icons/ui-icon'
import { UserAvatarForUsername } from '../../../../../common/user-avatar/user-avatar-for-username'
import type { PermissionDisabledProps } from './permission-disabled.prop'
import React, { Fragment } from 'react'
import { Button } from 'react-bootstrap'
import { Pencil as IconPencil } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
export interface PermissionOwnerInfoProps {
onEditOwner: () => void
@ -26,17 +26,13 @@ export const PermissionOwnerInfo: React.FC<PermissionOwnerInfoProps & Permission
onEditOwner,
disabled
}) => {
const { t } = useTranslation()
const noteOwner = useApplicationState((state) => state.noteDetails.permissions.owner)
const buttonTitle = useTranslatedText('editor.modal.permissions.ownerChange.button')
return (
<Fragment>
<UserAvatarForUsername username={noteOwner} />
<Button
variant='light'
disabled={disabled}
title={t('editor.modal.permissions.ownerChange.button') ?? undefined}
onClick={onEditOwner}>
<Button variant='light' disabled={disabled} title={buttonTitle} onClick={onEditOwner}>
<UiIcon icon={IconPencil} />
</Button>
</Fragment>

View file

@ -14,10 +14,12 @@ import { getUserDataForRevision } from './utils'
import { DateTime } from 'luxon'
import React, { useMemo } from 'react'
import { ListGroup } from 'react-bootstrap'
import { Clock as IconClock } from 'react-bootstrap-icons'
import { FileText as IconFileText } from 'react-bootstrap-icons'
import { Person as IconPerson } from 'react-bootstrap-icons'
import { PersonPlus as IconPersonPlus } from 'react-bootstrap-icons'
import {
Clock as IconClock,
FileText as IconFileText,
Person as IconPerson,
PersonPlus as IconPersonPlus
} from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
import { useAsync } from 'react-use'

View file

@ -13,8 +13,7 @@ import styles from './online-counter.module.scss'
import { OwnUserLine } from './own-user-line'
import { UserLine } from './user-line/user-line'
import React, { Fragment, useCallback, useEffect, useMemo, useRef } from 'react'
import { ArrowLeft as IconArrowLeft } from 'react-bootstrap-icons'
import { People as IconPeople } from 'react-bootstrap-icons'
import { ArrowLeft as IconArrowLeft, People as IconPeople } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
/**

View file

@ -9,9 +9,11 @@ import { UiIcon } from '../../../common/icons/ui-icon'
import styles from './split-divider.module.scss'
import React, { useMemo } from 'react'
import { Button } from 'react-bootstrap'
import { ArrowLeftRight as IconArrowLeftRight } from 'react-bootstrap-icons'
import { ArrowLeft as IconArrowLeft } from 'react-bootstrap-icons'
import { ArrowRight as IconArrowRight } from 'react-bootstrap-icons'
import {
ArrowLeft as IconArrowLeft,
ArrowLeftRight as IconArrowLeftRight,
ArrowRight as IconArrowRight
} from 'react-bootstrap-icons'
export enum DividerButtonsShift {
SHIFT_TO_LEFT = 'shift-left',

View file

@ -7,7 +7,7 @@ import { ShowIf } from '../../common/show-if/show-if'
import { useKeyboardShortcuts } from './hooks/use-keyboard-shortcuts'
import { DividerButtonsShift, SplitDivider } from './split-divider/split-divider'
import styles from './splitter.module.scss'
import type { ReactElement, TouchEvent, MouseEvent } from 'react'
import type { MouseEvent, ReactElement, TouchEvent } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
export interface SplitterProps {

View file

@ -13,9 +13,7 @@ import styles from './entry-menu.module.scss'
import { RemoveNoteEntryItem } from './remove-note-entry-item'
import React from 'react'
import { Dropdown } from 'react-bootstrap'
import { Cloud as IconCloud } from 'react-bootstrap-icons'
import { Laptop as IconLaptop } from 'react-bootstrap-icons'
import { ThreeDots as IconThreeDots } from 'react-bootstrap-icons'
import { Cloud as IconCloud, Laptop as IconLaptop, ThreeDots as IconThreeDots } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
export interface EntryMenuProps {

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useBooleanState } from '../../../hooks/common/use-boolean-state'
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import { deleteAllHistoryEntries } from '../../../redux/history/methods'
import { cypressId } from '../../../utils/cypress-attribute'
import { UiIcon } from '../../common/icons/ui-icon'
@ -13,14 +14,13 @@ import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-his
import React, { Fragment, useCallback } from 'react'
import { Button } from 'react-bootstrap'
import { Trash as IconTrash } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
import { Trans } from 'react-i18next'
/**
* Renders a button to clear the complete history of the user.
* A confirmation modal will be presented to the user after clicking the button.
*/
export const ClearHistoryButton: React.FC = () => {
const { t } = useTranslation()
const [modalVisibility, showModal, closeModal] = useBooleanState()
const { showErrorNotification } = useUiNotifications()
const safeRefreshHistoryState = useSafeRefreshHistoryStateCallback()
@ -33,13 +33,11 @@ export const ClearHistoryButton: React.FC = () => {
closeModal()
}, [closeModal, safeRefreshHistoryState, showErrorNotification])
const buttonTitle = useTranslatedText('landing.history.toolbar.clear')
return (
<Fragment>
<Button
variant={'secondary'}
title={t('landing.history.toolbar.clear') ?? undefined}
onClick={showModal}
{...cypressId('history-clear-button')}>
<Button variant={'secondary'} title={buttonTitle} onClick={showModal} {...cypressId('history-clear-button')}>
<UiIcon icon={IconTrash} />
</Button>
<DeletionModal

View file

@ -3,21 +3,21 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import { downloadHistory } from '../../../redux/history/methods'
import { UiIcon } from '../../common/icons/ui-icon'
import React from 'react'
import { Button } from 'react-bootstrap'
import { Download as IconDownload } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
/**
* Renders a button to export the history.
*/
export const ExportHistoryButton: React.FC = () => {
const { t } = useTranslation()
const buttonTitle = useTranslatedText('landing.history.toolbar.export')
return (
<Button variant={'secondary'} title={t('landing.history.toolbar.export') ?? undefined} onClick={downloadHistory}>
<Button variant={'secondary'} title={buttonTitle} onClick={downloadHistory}>
<UiIcon icon={IconDownload} />
</Button>
)

View file

@ -3,23 +3,22 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import { UiIcon } from '../../common/icons/ui-icon'
import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-history-state'
import React from 'react'
import { Button } from 'react-bootstrap'
import { ArrowRepeat as IconArrowRepeat } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
/**
* Fetches the current history from the server.
*/
export const HistoryRefreshButton: React.FC = () => {
const { t } = useTranslation()
const refreshHistory = useSafeRefreshHistoryStateCallback()
const buttonTitle = useTranslatedText('landing.history.toolbar.refresh')
return (
<Button variant={'secondary'} title={t('landing.history.toolbar.refresh') ?? undefined} onClick={refreshHistory}>
<Button variant={'secondary'} title={buttonTitle} onClick={refreshHistory}>
<UiIcon icon={IconArrowRepeat} />
</Button>
)

View file

@ -5,6 +5,7 @@
*/
import { HistoryEntryOrigin } from '../../../api/history/types'
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import { importHistoryEntries, setHistoryEntries } from '../../../redux/history/methods'
import { UiIcon } from '../../common/icons/ui-icon'
import { ShowIf } from '../../common/show-if/show-if'
@ -23,7 +24,6 @@ import { useSyncToolbarStateToUrlEffect } from './toolbar-context/use-sync-toolb
import React, { useCallback } from 'react'
import { Button, Col } from 'react-bootstrap'
import { CloudUpload as IconCloudUpload } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
export enum ViewStateEnum {
CARD,
@ -34,7 +34,6 @@ export enum ViewStateEnum {
* Renders the toolbar for the history page that contains controls for filtering and sorting.
*/
export const HistoryToolbar: React.FC = () => {
const { t } = useTranslation()
const historyEntries = useApplicationState((state) => state.history)
const userExists = useApplicationState((state) => !!state.user)
const { showErrorNotification } = useUiNotifications()
@ -61,6 +60,8 @@ export const HistoryToolbar: React.FC = () => {
})
}, [userExists, historyEntries, showErrorNotification, safeRefreshHistoryState])
const uploadAllButtonTitle = useTranslatedText('landing.history.toolbar.uploadAll')
return (
<Col className={'d-flex flex-row'}>
<div className={'me-1 mb-1'}>
@ -89,10 +90,7 @@ export const HistoryToolbar: React.FC = () => {
</div>
<ShowIf condition={userExists}>
<div className={'me-1 mb-1'}>
<Button
variant={'secondary'}
title={t('landing.history.toolbar.uploadAll') ?? undefined}
onClick={onUploadAllToRemote}>
<Button variant={'secondary'} title={uploadAllButtonTitle} onClick={onUploadAllToRemote}>
<UiIcon icon={IconCloudUpload} />
</Button>
</div>

View file

@ -3,21 +3,19 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import { cypressId } from '../../../utils/cypress-attribute'
import { UiIcon } from '../../common/icons/ui-icon'
import { ViewStateEnum } from './history-toolbar'
import { useHistoryToolbarState } from './toolbar-context/use-history-toolbar-state'
import React, { useCallback } from 'react'
import { Button, ToggleButtonGroup } from 'react-bootstrap'
import { StickyFill as IconStickyFill } from 'react-bootstrap-icons'
import { Table as IconTable } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
import { StickyFill as IconStickyFill, Table as IconTable } from 'react-bootstrap-icons'
/**
* Toggles the view mode of the history entries between list and card view.
*/
export const HistoryViewModeToggleButton: React.FC = () => {
const { t } = useTranslation()
const [historyToolbarState, setHistoryToolbarState] = useHistoryToolbarState()
const onViewStateChange = useCallback(
@ -30,19 +28,25 @@ export const HistoryViewModeToggleButton: React.FC = () => {
[setHistoryToolbarState]
)
const cardsButtonTitle = useTranslatedText('landing.history.toolbar.cards')
const tableButtonTitle = useTranslatedText('landing.history.toolbar.table')
const onCardsButtonClick = useCallback(() => onViewStateChange(ViewStateEnum.CARD), [onViewStateChange])
const onTableButtonClick = useCallback(() => onViewStateChange(ViewStateEnum.TABLE), [onViewStateChange])
return (
<ToggleButtonGroup type='radio' name='options' dir='auto' className={'button-height'} onChange={onViewStateChange}>
<Button
title={t('landing.history.toolbar.cards') ?? undefined}
title={cardsButtonTitle}
variant={historyToolbarState.viewState === ViewStateEnum.CARD ? 'secondary' : 'outline-secondary'}
onClick={() => onViewStateChange(ViewStateEnum.CARD)}>
onClick={onCardsButtonClick}>
<UiIcon icon={IconStickyFill} className={'fa-fix-line-height'} />
</Button>
<Button
{...cypressId('history-mode-table')}
variant={historyToolbarState.viewState === ViewStateEnum.TABLE ? 'secondary' : 'outline-secondary'}
title={t('landing.history.toolbar.table') ?? undefined}
onClick={() => onViewStateChange(ViewStateEnum.TABLE)}>
title={tableButtonTitle}
onClick={onTableButtonClick}>
<UiIcon icon={IconTable} className={'fa-fix-line-height'} />
</Button>
</ToggleButtonGroup>

View file

@ -6,6 +6,7 @@
import type { HistoryEntryWithOrigin } from '../../../api/history/types'
import { HistoryEntryOrigin } from '../../../api/history/types'
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import { convertV1History, importHistoryEntries, mergeHistoryEntries } from '../../../redux/history/methods'
import type { HistoryExportJson, V1HistoryEntry } from '../../../redux/history/types'
import { cypressId } from '../../../utils/cypress-attribute'
@ -15,13 +16,11 @@ import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-his
import React, { useCallback, useRef, useState } from 'react'
import { Button } from 'react-bootstrap'
import { Upload as IconUpload } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
/**
* Button that lets the user select a history JSON file and uploads imports that into the history.
*/
export const ImportHistoryButton: React.FC = () => {
const { t } = useTranslation()
const userExists = useApplicationState((state) => !!state.user)
const historyState = useApplicationState((state) => state.history)
const uploadInput = useRef<HTMLInputElement>(null)
@ -117,6 +116,8 @@ export const ImportHistoryButton: React.FC = () => {
}
}
const buttonTitle = useTranslatedText('landing.history.toolbar.import')
return (
<div>
<input
@ -129,7 +130,7 @@ export const ImportHistoryButton: React.FC = () => {
/>
<Button
variant={'secondary'}
title={t('landing.history.toolbar.import') ?? undefined}
title={buttonTitle}
onClick={onUploadButtonClick}
{...cypressId('import-history-file-button')}>
<UiIcon icon={IconUpload} />

View file

@ -4,16 +4,15 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import { useHistoryToolbarState } from './toolbar-context/use-history-toolbar-state'
import React from 'react'
import { FormControl } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
/**
* A text input that is used to filter history entries for specific keywords.
*/
export const KeywordSearchInput: React.FC = () => {
const { t } = useTranslation()
const [historyToolbarState, setHistoryToolbarState] = useHistoryToolbarState()
const onChange = useOnInputChange((search) => {
@ -23,10 +22,12 @@ export const KeywordSearchInput: React.FC = () => {
}))
})
const searchKeywordsText = useTranslatedText('landing.history.toolbar.searchKeywords')
return (
<FormControl
placeholder={t('landing.history.toolbar.searchKeywords') ?? undefined}
aria-label={t('landing.history.toolbar.searchKeywords') ?? undefined}
placeholder={searchKeywordsText}
aria-label={searchKeywordsText}
onChange={onChange}
value={historyToolbarState.search}
/>

View file

@ -4,17 +4,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import { useHistoryToolbarState } from './toolbar-context/use-history-toolbar-state'
import React, { useCallback, useMemo } from 'react'
import { Typeahead } from 'react-bootstrap-typeahead'
import type { Option } from 'react-bootstrap-typeahead/types/types'
import { useTranslation } from 'react-i18next'
/**
* Renders an input field that filters history entries by selected tags.
*/
export const TagSelectionInput: React.FC = () => {
const { t } = useTranslation()
const [historyToolbarState, setHistoryToolbarState] = useHistoryToolbarState()
const historyEntries = useApplicationState((state) => state.history)
@ -37,12 +36,13 @@ export const TagSelectionInput: React.FC = () => {
[setHistoryToolbarState]
)
const placeholderText = useTranslatedText('landing.history.toolbar.selectTags')
return (
<Typeahead
id={'tagsSelection'}
options={tags}
multiple={true}
placeholder={t('landing.history.toolbar.selectTags') ?? undefined}
placeholder={placeholderText}
onChange={onChange}
selected={historyToolbarState.selectedTags}
/>

View file

@ -6,9 +6,7 @@
import { IconButton } from '../../common/icon-button/icon-button'
import React, { useCallback, useMemo } from 'react'
import type { ButtonProps } from 'react-bootstrap'
import { SortAlphaDown as IconSortAlphaDown } from 'react-bootstrap-icons'
import { SortAlphaUp as IconSortAlphaUp } from 'react-bootstrap-icons'
import { X as IconX } from 'react-bootstrap-icons'
import { SortAlphaDown as IconSortAlphaDown, SortAlphaUp as IconSortAlphaUp, X as IconX } from 'react-bootstrap-icons'
export enum SortModeEnum {
up = 1,

View file

@ -8,9 +8,7 @@ import { IconDiscourse } from '../../common/icons/additional/icon-discourse'
import { IconMatrixOrg } from '../../common/icons/additional/icon-matrix-org'
import { ExternalLink } from '../../common/links/external-link'
import React from 'react'
import { Github as IconGithub } from 'react-bootstrap-icons'
import { Globe as IconGlobe } from 'react-bootstrap-icons'
import { Mastodon as IconMastodon } from 'react-bootstrap-icons'
import { Github as IconGithub, Globe as IconGlobe, Mastodon as IconMastodon } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
/**

View file

@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import { cypressId } from '../../../utils/cypress-attribute'
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
import { ShowIf } from '../../common/show-if/show-if'
@ -12,7 +13,7 @@ import Link from 'next/link'
import React, { useMemo } from 'react'
import { Button } from 'react-bootstrap'
import type { ButtonProps } from 'react-bootstrap/Button'
import { Trans, useTranslation } from 'react-i18next'
import { Trans } from 'react-i18next'
export type SignInButtonProps = Omit<ButtonProps, 'href'>
@ -24,7 +25,6 @@ export type SignInButtonProps = Omit<ButtonProps, 'href'>
* @param props Further props inferred from the common button component.
*/
export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props }) => {
const { t } = useTranslation()
const authProviders = useFrontendConfig().authProviders
const loginLink = useMemo(() => {
@ -35,15 +35,12 @@ export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props })
}
return '/login'
}, [authProviders])
const buttonTitle = useTranslatedText('login.signIn')
return (
<ShowIf condition={authProviders.length > 0}>
<Link href={loginLink} passHref={true}>
<Button
title={t('login.signIn') ?? undefined}
{...cypressId('sign-in-button')}
variant={variant || 'success'}
{...props}>
<Button title={buttonTitle} {...cypressId('sign-in-button')} variant={variant || 'success'} {...props}>
<Trans i18nKey='login.signIn' />
</Button>
</Link>

View file

@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import type { CommonModalProps } from '../../common/modals/common-modal'
import { CommonModal } from '../../common/modals/common-modal'
import { EditorSettingsTabContent } from './editor/editor-settings-tab-content'
@ -10,7 +11,6 @@ import { GlobalSettingsTabContent } from './global/global-settings-tab-content'
import React from 'react'
import { Modal, Tab, Tabs } from 'react-bootstrap'
import { Gear as IconGear } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
/**
* Shows global and scope specific settings
@ -19,7 +19,8 @@ import { useTranslation } from 'react-i18next'
* @param onHide callback that is executed if the modal should be closed
*/
export const SettingsModal: React.FC<CommonModalProps> = ({ show, onHide }) => {
const { t } = useTranslation()
const globalLabelTitle = useTranslatedText('settings.global.label')
const editorLabelTitle = useTranslatedText('settings.editor.label')
return (
<CommonModal
@ -31,10 +32,10 @@ export const SettingsModal: React.FC<CommonModalProps> = ({ show, onHide }) => {
showCloseButton={true}>
<Modal.Body>
<Tabs navbar={false} variant={'tabs'} defaultActiveKey={'global'}>
<Tab title={t('settings.global.label')} eventKey={'global'}>
<Tab title={globalLabelTitle} eventKey={'global'}>
<GlobalSettingsTabContent />
</Tab>
<Tab title={t('settings.editor.label')} eventKey={'editor'}>
<Tab title={editorLabelTitle} eventKey={'editor'}>
<EditorSettingsTabContent />
</Tab>
</Tabs>

View file

@ -3,11 +3,12 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../../hooks/common/use-translated-text'
import type { PropsWithDataTestId } from '../../../../utils/test-id'
import React, { useCallback, useMemo } from 'react'
import React, { useCallback } from 'react'
import type { ButtonProps } from 'react-bootstrap'
import { Button } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { Trans } from 'react-i18next'
type DarkModeToggleButtonProps = Omit<ButtonProps, 'onSelect'> &
PropsWithDataTestId & {
@ -36,9 +37,7 @@ export const SettingsToggleButton = ({
value,
...props
}: DarkModeToggleButtonProps) => {
const { t } = useTranslation()
const title = useMemo(() => t(i18nKeyTooltip), [i18nKeyTooltip, t])
const title = useTranslatedText(i18nKeyTooltip)
const onChange = useCallback(() => {
if (!selected) {

View file

@ -3,10 +3,10 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../../hooks/common/use-translated-text'
import type { AuthFieldProps } from './fields'
import React from 'react'
import { Form } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
/**
* Renders an input field for the password of a user.
@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next'
* @param invalid True when the entered password is invalid, false otherwise.
*/
export const PasswordField: React.FC<AuthFieldProps> = ({ onChange, invalid }) => {
const { t } = useTranslation()
const placeholderText = useTranslatedText('login.auth.password')
return (
<Form.Group>
@ -23,7 +23,7 @@ export const PasswordField: React.FC<AuthFieldProps> = ({ onChange, invalid }) =
isInvalid={invalid}
type='password'
size='sm'
placeholder={t('login.auth.password') ?? undefined}
placeholder={placeholderText}
onChange={onChange}
autoComplete='current-password'
/>

View file

@ -9,14 +9,16 @@ import { Logger } from '../../../../utils/logger'
import { IconGitlab } from '../../../common/icons/additional/icon-gitlab'
import styles from '../via-one-click.module.scss'
import type { Icon } from 'react-bootstrap-icons'
import { Dropbox as IconDropbox } from 'react-bootstrap-icons'
import { Exclamation as IconExclamation } from 'react-bootstrap-icons'
import { Facebook as IconFacebook } from 'react-bootstrap-icons'
import { Github as IconGithub } from 'react-bootstrap-icons'
import { Google as IconGoogle } from 'react-bootstrap-icons'
import { People as IconPeople } from 'react-bootstrap-icons'
import { PersonRolodex as IconPersonRolodex } from 'react-bootstrap-icons'
import { Twitter as IconTwitter } from 'react-bootstrap-icons'
import {
Dropbox as IconDropbox,
Exclamation as IconExclamation,
Facebook as IconFacebook,
Github as IconGithub,
Google as IconGoogle,
People as IconPeople,
PersonRolodex as IconPersonRolodex,
Twitter as IconTwitter
} from 'react-bootstrap-icons'
export interface OneClickMetadata {
name: string

View file

@ -14,7 +14,7 @@ import { PasswordField } from './fields/password-field'
import { fetchAndSetUser } from './utils'
import Link from 'next/link'
import type { FormEvent } from 'react'
import React, { useCallback, useState } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import { Alert, Button, Card, Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
@ -48,11 +48,13 @@ export const ViaLocal: React.FC = () => {
const onUsernameChange = useLowercaseOnInputChange(setUsername)
const onPasswordChange = useOnInputChange(setPassword)
const translationOptions = useMemo(() => ({ service: t('login.auth.username') }), [t])
return (
<Card className='mb-4'>
<Card.Body>
<Card.Title>
<Trans i18nKey='login.signInVia' values={{ service: t('login.auth.username') }} />
<Trans i18nKey='login.signInVia' values={translationOptions} />
</Card.Title>
<Form onSubmit={onLoginSubmit} className={'d-flex gap-3 flex-column'}>
<UsernameField onChange={onUsernameChange} isInvalid={!!error} value={username} />

View file

@ -3,11 +3,12 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../../hooks/common/use-translated-text'
import { cypressId } from '../../../../utils/cypress-attribute'
import type { ChangeEvent } from 'react'
import React, { useMemo } from 'react'
import { Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { Trans } from 'react-i18next'
interface AccessTokenCreationFormLabelFieldProps extends AccessTokenCreationFormFieldProps {
onChangeLabel: (event: ChangeEvent<HTMLInputElement>) => void
@ -23,11 +24,8 @@ export const AccessTokenCreationFormLabelField: React.FC<AccessTokenCreationForm
onChangeLabel,
formValues
}) => {
const { t } = useTranslation()
const labelValid = useMemo(() => {
return formValues.label.trim() !== ''
}, [formValues])
const labelValid = useMemo(() => formValues.label.trim() !== '', [formValues])
const placeholderText = useTranslatedText('profile.accessTokens.label')
return (
<Form.Group>
@ -37,7 +35,7 @@ export const AccessTokenCreationFormLabelField: React.FC<AccessTokenCreationForm
<Form.Control
type='text'
size='sm'
placeholder={t('profile.accessTokens.label') ?? undefined}
placeholder={placeholderText}
value={formValues.label}
onChange={onChangeLabel}
isValid={labelValid}

View file

@ -8,8 +8,7 @@ import { UiIcon } from '../../common/icons/ui-icon'
import { AccountDeletionModal } from './account-deletion-modal'
import React, { Fragment } from 'react'
import { Button, Card, Row } from 'react-bootstrap'
import { Trash as IconTrash } from 'react-bootstrap-icons'
import { CloudDownload as IconCloudDownload } from 'react-bootstrap-icons'
import { CloudDownload as IconCloudDownload, Trash as IconTrash } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
/**

View file

@ -5,7 +5,7 @@
*/
import { TestMarkdownRenderer } from '../../../components/markdown-renderer/test-utils/test-markdown-renderer'
import { BlockquoteExtraTagMarkdownExtension } from './blockquote-extra-tag-markdown-extension'
import { screen, render } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import React from 'react'
describe('blockquote extra tag', () => {

View file

@ -8,8 +8,7 @@ import { codeFenceRegex } from '../../../components/editor-page/editor-pane/auto
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/_base-classes/markdown-renderer-extension'
import { AppExtension } from '../../_base-classes/app-extension'
import { HighlightedCodeMarkdownExtension } from './highlighted-code-markdown-extension'
import type { CompletionSource } from '@codemirror/autocomplete'
import type { CompletionContext, CompletionResult } from '@codemirror/autocomplete'
import type { CompletionContext, CompletionResult, CompletionSource } from '@codemirror/autocomplete'
import { languages } from '@codemirror/language-data'
/**

View file

@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as HighlightedCodeModule from '../../../components/common/highlighted-code/highlighted-code'
import type { HighlightedCodeProps } from '../../../components/common/highlighted-code/highlighted-code'
import * as HighlightedCodeModule from '../../../components/common/highlighted-code/highlighted-code'
import { TestMarkdownRenderer } from '../../../components/markdown-renderer/test-utils/test-markdown-renderer'
import { mockI18n } from '../../../test-utils/mock-i18n'
import { HighlightedCodeMarkdownExtension } from './highlighted-code-markdown-extension'

View file

@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
import type { ClickShieldProps } from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
import { AsciinemaFrame } from './asciinema-frame'
import { render } from '@testing-library/react'
import React from 'react'

View file

@ -49,7 +49,7 @@ export const VegaLiteChart: React.FC<CodeProps> = ({ code }) => {
SVG_ACTION: t('renderer.vega-lite.svg') ?? undefined
}
})
}, [code, vegaEmbed])
}, [code, vegaEmbed, t])
useEffect(() => {
if (renderingError) {

View file

@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
import type { ClickShieldProps } from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
import { VimeoFrame } from './vimeo-frame'
import { render } from '@testing-library/react'
import React from 'react'

View file

@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
import type { ClickShieldProps } from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
import { YouTubeFrame } from './youtube-frame'
import { render } from '@testing-library/react'
import React from 'react'

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from './use-application-state'
import { useTranslatedText } from './use-translated-text'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
/**
* Retrieves the title of the note or a placeholder text, if no title is set.
@ -13,8 +13,7 @@ import { useTranslation } from 'react-i18next'
* @return The title of the note
*/
export const useNoteTitle = (): string => {
const { t } = useTranslation()
const untitledNote = useMemo(() => t('editor.untitledNote'), [t])
const untitledNote = useTranslatedText('editor.untitledNote')
const noteTitle = useApplicationState((state) => state.noteDetails.title)
return useMemo(() => (noteTitle === '' ? untitledNote : noteTitle), [noteTitle, untitledNote])

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { TOptions } from 'i18next'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
/**
* Translates text and caches it.
*
* @param key The translation key
* @param tOptions Optional translation options
* @return the translated text
*/
export const useTranslatedText = (key: string, tOptions?: TOptions): string => {
const { t } = useTranslation()
return useMemo(() => (tOptions ? t(key, tOptions) : t(key)), [key, tOptions, t])
}

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from './use-translated-text'
import { renderHook } from '@testing-library/react'
import type { Namespace } from 'i18next'
import * as ReactI18NextModule from 'react-i18next'
import type { UseTranslationResponse } from 'react-i18next'
import { Mock } from 'ts-mockery'
jest.mock('react-i18next')
describe('useTranslatedText', () => {
const mockTranslation = 'mockTranslation'
const mockKey = 'mockKey'
let translateFunction: jest.Mock
beforeEach(() => {
translateFunction = jest.fn(() => mockTranslation)
const useTranslateMock = Mock.of<UseTranslationResponse<Namespace, unknown>>({
t: translateFunction
})
jest.spyOn(ReactI18NextModule, 'useTranslation').mockReturnValue(useTranslateMock)
})
it('translates text', () => {
const { result } = renderHook(() => useTranslatedText(mockKey))
expect(result.current).toBe(mockTranslation)
expect(translateFunction).toBeCalledWith(mockKey)
})
it('translates text with options', () => {
const mockOptions = {}
const { result } = renderHook(() => useTranslatedText(mockKey, mockOptions))
expect(result.current).toBe(mockTranslation)
expect(translateFunction).toBeCalledWith(mockKey, mockOptions)
})
})

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { store } from '..'
import type { SetRealtimeSyncStatusAction, SetRealtimeUsersAction, SetRealtimeConnectionStatusAction } from './types'
import type { SetRealtimeConnectionStatusAction, SetRealtimeSyncStatusAction, SetRealtimeUsersAction } from './types'
import { RealtimeStatusActionType } from './types'
import type { RealtimeUser } from '@hedgedoc/commons'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { RealtimeStatusActions, RealtimeStatus } from './types'
import type { RealtimeStatus, RealtimeStatusActions } from './types'
import { RealtimeStatusActionType } from './types'
import type { Reducer } from 'redux'

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useBaseUrl } from '../hooks/common/use-base-url'
import React, { Fragment, useMemo } from 'react'
import type { PropsWithChildren } from 'react'
import React, { Fragment, useMemo } from 'react'
export interface ExpectedOriginBoundaryProps {
currentOrigin?: string