mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-15 15:44:45 -04:00
Enhance share dialog (#860)
This commit is contained in:
parent
1c6e6e10fb
commit
721c8c0e5a
10 changed files with 120 additions and 25 deletions
|
@ -57,6 +57,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
- Markdown files can be imported into an existing note directly from the editor.
|
- Markdown files can be imported into an existing note directly from the editor.
|
||||||
- The table button in the toolbar opens an overlay where the user can choose the number of columns and rows
|
- The table button in the toolbar opens an overlay where the user can choose the number of columns and rows
|
||||||
- A toggle in the editor preferences for turning ligatures on and off.
|
- A toggle in the editor preferences for turning ligatures on and off.
|
||||||
|
- Easier possibility to share notes via native share-buttons on supported devices.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
|
@ -389,7 +389,9 @@
|
||||||
},
|
},
|
||||||
"shareLink": {
|
"shareLink": {
|
||||||
"title": "Share link",
|
"title": "Share link",
|
||||||
"viewOnlyDescription": "This link points to a read-only version of this note. You can use this e.g. for feedback from friends and colleagues."
|
"editorDescription": "This link points to this note in the editor as you currently see it.",
|
||||||
|
"viewOnlyDescription": "This link points to a read-only version of this note. You can use this e.g. for feedback from friends and colleagues.",
|
||||||
|
"slidesDescription": "This link points to the presentation view of the slides."
|
||||||
},
|
},
|
||||||
"preferences": {
|
"preferences": {
|
||||||
"title": "Preferences",
|
"title": "Preferences",
|
||||||
|
|
|
@ -4,31 +4,52 @@ SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Fragment, useRef } from 'react'
|
import React, { Fragment, useCallback, useRef } from 'react'
|
||||||
import { Button, FormControl, InputGroup } from 'react-bootstrap'
|
import { Button, FormControl, InputGroup } from 'react-bootstrap'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ForkAwesomeIcon } from '../../fork-awesome/fork-awesome-icon'
|
import { ForkAwesomeIcon } from '../../fork-awesome/fork-awesome-icon'
|
||||||
|
import { ShowIf } from '../../show-if/show-if'
|
||||||
import { CopyOverlay } from '../copy-overlay'
|
import { CopyOverlay } from '../copy-overlay'
|
||||||
|
|
||||||
export interface CopyableFieldProps {
|
export interface CopyableFieldProps {
|
||||||
content: string
|
content: string
|
||||||
|
nativeShareButton?: boolean
|
||||||
|
url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CopyableField: React.FC<CopyableFieldProps> = ({ content }) => {
|
export const CopyableField: React.FC<CopyableFieldProps> = ({ content, nativeShareButton, url }) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const button = useRef<HTMLButtonElement>(null)
|
const copyButton = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
|
const doShareAction = useCallback(() => {
|
||||||
|
navigator.share({
|
||||||
|
text: content,
|
||||||
|
url: url
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Native sharing failed: ', err)
|
||||||
|
})
|
||||||
|
}, [content, url])
|
||||||
|
|
||||||
|
const sharingSupported = typeof navigator.share === 'function'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<InputGroup className="my-3">
|
<InputGroup className="my-3">
|
||||||
<FormControl readOnly={true} className={'text-center'} value={content} />
|
<FormControl readOnly={true} className={'text-center'} value={content} />
|
||||||
<InputGroup.Append>
|
<InputGroup.Append>
|
||||||
<Button variant="outline-secondary" ref={button} title={'Copy'}>
|
<Button variant="outline-secondary" ref={copyButton} title={'Copy'}>
|
||||||
<ForkAwesomeIcon icon='files-o'/>
|
<ForkAwesomeIcon icon='files-o'/>
|
||||||
</Button>
|
</Button>
|
||||||
</InputGroup.Append>
|
</InputGroup.Append>
|
||||||
|
<ShowIf condition={!!nativeShareButton && sharingSupported}>
|
||||||
|
<InputGroup.Append>
|
||||||
|
<Button variant="outline-secondary" title={'Share'} onClick={doShareAction}>
|
||||||
|
<ForkAwesomeIcon icon='share-alt'/>
|
||||||
|
</Button>
|
||||||
|
</InputGroup.Append>
|
||||||
|
</ShowIf>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<CopyOverlay content={content} clickComponent={button}/>
|
<CopyOverlay content={content} clickComponent={copyButton}/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,9 @@ import { setEditorMode } from '../../../redux/editor/methods'
|
||||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||||
|
|
||||||
export enum EditorMode {
|
export enum EditorMode {
|
||||||
PREVIEW,
|
PREVIEW = 'view',
|
||||||
BOTH,
|
BOTH = 'both',
|
||||||
EDITOR,
|
EDITOR = 'edit',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditorViewMode: React.FC = () => {
|
export const EditorViewMode: React.FC = () => {
|
||||||
|
|
|
@ -4,27 +4,54 @@ SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
|
||||||
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 { Modal } from 'react-bootstrap'
|
import { Modal } from 'react-bootstrap'
|
||||||
import { Trans } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { useFrontendBaseUrl } from '../../../../hooks/common/use-frontend-base-url'
|
||||||
|
import { ApplicationState } from '../../../../redux'
|
||||||
import { CopyableField } from '../../../common/copyable/copyable-field/copyable-field'
|
import { CopyableField } from '../../../common/copyable/copyable-field/copyable-field'
|
||||||
import { TranslatedIconButton } from '../../../common/icon-button/translated-icon-button'
|
import { TranslatedIconButton } from '../../../common/icon-button/translated-icon-button'
|
||||||
import { CommonModal } from '../../../common/modals/common-modal'
|
import { CommonModal } from '../../../common/modals/common-modal'
|
||||||
|
import { ShowIf } from '../../../common/show-if/show-if'
|
||||||
|
import { EditorPathParams } from '../../editor'
|
||||||
|
|
||||||
export const ShareLinkButton: React.FC = () => {
|
export const ShareLinkButton: React.FC = () => {
|
||||||
const [showReadOnly, setShowReadOnly] = useState(false)
|
useTranslation()
|
||||||
|
const [showShareDialog, setShowShareDialog] = useState(false)
|
||||||
|
const noteMetadata = useSelector((state: ApplicationState) => state.documentContent.metadata, equal)
|
||||||
|
const editorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
|
||||||
|
const baseUrl = useFrontendBaseUrl()
|
||||||
|
const { id } = useParams<EditorPathParams>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<TranslatedIconButton size={'sm'} className={'mx-1'} icon={'share'} variant={'light'} onClick={() => setShowReadOnly(true)} i18nKey={'editor.documentBar.shareLink'}/>
|
<TranslatedIconButton
|
||||||
|
size={'sm'}
|
||||||
|
className={'mx-1'}
|
||||||
|
icon={'share'}
|
||||||
|
variant={'light'}
|
||||||
|
onClick={() => setShowShareDialog(true)}
|
||||||
|
i18nKey={'editor.documentBar.shareLink'}
|
||||||
|
/>
|
||||||
<CommonModal
|
<CommonModal
|
||||||
show={showReadOnly}
|
show={showShareDialog}
|
||||||
onHide={() => setShowReadOnly(false)}
|
onHide={() => setShowShareDialog(false)}
|
||||||
closeButton={true}
|
closeButton={true}
|
||||||
titleI18nKey={'editor.modal.shareLink.title'}>
|
titleI18nKey={'editor.modal.shareLink.title'}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<span className={'my-4'}><Trans i18nKey={'editor.modal.shareLink.viewOnlyDescription'}/></span>
|
<Trans i18nKey={'editor.modal.shareLink.editorDescription'}/>
|
||||||
<CopyableField content={'https://example.com'}/>
|
<CopyableField content={`${baseUrl}/n/${id}?${editorMode}`} nativeShareButton={true} url={`${baseUrl}/n/${id}?${editorMode}`}/>
|
||||||
|
<ShowIf condition={noteMetadata.type === 'slide'}>
|
||||||
|
<Trans i18nKey={'editor.modal.shareLink.slidesDescription'}/>
|
||||||
|
<CopyableField content={`${baseUrl}/p/${id}`} nativeShareButton={true} url={`${baseUrl}/p/${id}`}/>
|
||||||
|
</ShowIf>
|
||||||
|
<ShowIf condition={noteMetadata.type === ''}>
|
||||||
|
<Trans i18nKey={'editor.modal.shareLink.viewOnlyDescription'}/>
|
||||||
|
<CopyableField content={`${baseUrl}/s/${id}`} nativeShareButton={true} url={`${baseUrl}/s/${id}`}/>
|
||||||
|
</ShowIf>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
</CommonModal>
|
</CommonModal>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|
|
@ -12,7 +12,7 @@ import useMedia from 'use-media'
|
||||||
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
||||||
import { useDocumentTitle } from '../../hooks/common/use-document-title'
|
import { useDocumentTitle } from '../../hooks/common/use-document-title'
|
||||||
import { ApplicationState } from '../../redux'
|
import { ApplicationState } from '../../redux'
|
||||||
import { setDocumentContent, setNoteId } from '../../redux/document-content/methods'
|
import { setDocumentContent, setDocumentMetadata, setNoteId } from '../../redux/document-content/methods'
|
||||||
import { setEditorMode } from '../../redux/editor/methods'
|
import { setEditorMode } from '../../redux/editor/methods'
|
||||||
import { extractNoteTitle } from '../common/document-title/note-title-extractor'
|
import { extractNoteTitle } from '../common/document-title/note-title-extractor'
|
||||||
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
||||||
|
@ -74,6 +74,7 @@ export const Editor: React.FC = () => {
|
||||||
|
|
||||||
const onMetadataChange = useCallback((metaData: YAMLMetaData | undefined) => {
|
const onMetadataChange = useCallback((metaData: YAMLMetaData | undefined) => {
|
||||||
noteMetadata.current = metaData
|
noteMetadata.current = metaData
|
||||||
|
setDocumentMetadata(metaData)
|
||||||
updateDocumentTitle()
|
updateDocumentTitle()
|
||||||
}, [updateDocumentTitle])
|
}, [updateDocumentTitle])
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { useParams } from 'react-router'
|
||||||
import { getNote, Note } from '../../api/notes'
|
import { getNote, Note } from '../../api/notes'
|
||||||
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
||||||
import { useDocumentTitle } from '../../hooks/common/use-document-title'
|
import { useDocumentTitle } from '../../hooks/common/use-document-title'
|
||||||
import { setDocumentContent } from '../../redux/document-content/methods'
|
import { setDocumentContent, setDocumentMetadata } from '../../redux/document-content/methods'
|
||||||
import { extractNoteTitle } from '../common/document-title/note-title-extractor'
|
import { extractNoteTitle } from '../common/document-title/note-title-extractor'
|
||||||
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'
|
||||||
|
@ -44,6 +44,7 @@ export const PadViewOnly: React.FC = () => {
|
||||||
|
|
||||||
const onMetadataChange = useCallback((metaData: YAMLMetaData | undefined) => {
|
const onMetadataChange = useCallback((metaData: YAMLMetaData | undefined) => {
|
||||||
noteMetadata.current = metaData
|
noteMetadata.current = metaData
|
||||||
|
setDocumentMetadata(metaData)
|
||||||
updateDocumentTitle()
|
updateDocumentTitle()
|
||||||
}, [updateDocumentTitle])
|
}, [updateDocumentTitle])
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,18 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { store } from '..'
|
import { store } from '..'
|
||||||
import { DocumentContentActionType, SetDocumentContentAction, SetNoteIdAction } from './types'
|
import { YAMLMetaData } from '../../components/editor/yaml-metadata/yaml-metadata'
|
||||||
|
import {
|
||||||
|
DocumentContentActionType,
|
||||||
|
SetDocumentContentAction,
|
||||||
|
SetDocumentMetadataAction,
|
||||||
|
SetNoteIdAction
|
||||||
|
} from './types'
|
||||||
|
|
||||||
export const setDocumentContent = (content: string): void => {
|
export const setDocumentContent = (content: string): void => {
|
||||||
const action: SetDocumentContentAction = {
|
const action: SetDocumentContentAction = {
|
||||||
type: DocumentContentActionType.SET_DOCUMENT_CONTENT,
|
type: DocumentContentActionType.SET_DOCUMENT_CONTENT,
|
||||||
content: content
|
content
|
||||||
}
|
}
|
||||||
store.dispatch(action)
|
store.dispatch(action)
|
||||||
}
|
}
|
||||||
|
@ -18,7 +24,18 @@ export const setDocumentContent = (content: string): void => {
|
||||||
export const setNoteId = (noteId: string): void => {
|
export const setNoteId = (noteId: string): void => {
|
||||||
const action: SetNoteIdAction = {
|
const action: SetNoteIdAction = {
|
||||||
type: DocumentContentActionType.SET_NOTE_ID,
|
type: DocumentContentActionType.SET_NOTE_ID,
|
||||||
noteId: noteId
|
noteId
|
||||||
|
}
|
||||||
|
store.dispatch(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setDocumentMetadata = (metadata: YAMLMetaData | undefined): void => {
|
||||||
|
if (!metadata) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const action: SetDocumentMetadataAction = {
|
||||||
|
type: DocumentContentActionType.SET_DOCUMENT_METADATA,
|
||||||
|
metadata
|
||||||
}
|
}
|
||||||
store.dispatch(action)
|
store.dispatch(action)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,13 +9,26 @@ import {
|
||||||
DocumentContent,
|
DocumentContent,
|
||||||
DocumentContentAction,
|
DocumentContentAction,
|
||||||
DocumentContentActionType,
|
DocumentContentActionType,
|
||||||
SetDocumentContentAction,
|
SetDocumentContentAction, SetDocumentMetadataAction,
|
||||||
SetNoteIdAction
|
SetNoteIdAction
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
export const initialState: DocumentContent = {
|
export const initialState: DocumentContent = {
|
||||||
content: '',
|
content: '',
|
||||||
noteId: ''
|
noteId: '',
|
||||||
|
metadata: {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
tags: [],
|
||||||
|
robots: '',
|
||||||
|
lang: 'en',
|
||||||
|
dir: 'ltr',
|
||||||
|
breaks: true,
|
||||||
|
GA: '',
|
||||||
|
disqus: '',
|
||||||
|
type: '',
|
||||||
|
opengraph: new Map<string, string>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocumentContentReducer: Reducer<DocumentContent, DocumentContentAction> = (state: DocumentContent = initialState, action: DocumentContentAction) => {
|
export const DocumentContentReducer: Reducer<DocumentContent, DocumentContentAction> = (state: DocumentContent = initialState, action: DocumentContentAction) => {
|
||||||
|
@ -30,6 +43,11 @@ export const DocumentContentReducer: Reducer<DocumentContent, DocumentContentAct
|
||||||
...state,
|
...state,
|
||||||
noteId: (action as SetNoteIdAction).noteId
|
noteId: (action as SetNoteIdAction).noteId
|
||||||
}
|
}
|
||||||
|
case DocumentContentActionType.SET_DOCUMENT_METADATA:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
metadata: (action as SetDocumentMetadataAction).metadata
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,15 +5,18 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Action } from 'redux'
|
import { Action } from 'redux'
|
||||||
|
import { YAMLMetaData } from '../../components/editor/yaml-metadata/yaml-metadata'
|
||||||
|
|
||||||
export enum DocumentContentActionType {
|
export enum DocumentContentActionType {
|
||||||
SET_DOCUMENT_CONTENT = 'document-content/set',
|
SET_DOCUMENT_CONTENT = 'document-content/set',
|
||||||
SET_NOTE_ID = 'document-content/noteid/set'
|
SET_NOTE_ID = 'document-content/noteid/set',
|
||||||
|
SET_DOCUMENT_METADATA = 'document-content/metadata/set'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentContent {
|
export interface DocumentContent {
|
||||||
content: string
|
content: string
|
||||||
noteId: string
|
noteId: string,
|
||||||
|
metadata: YAMLMetaData
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentContentAction extends Action<DocumentContentActionType> {
|
export interface DocumentContentAction extends Action<DocumentContentActionType> {
|
||||||
|
@ -27,3 +30,7 @@ export interface SetDocumentContentAction extends DocumentContentAction {
|
||||||
export interface SetNoteIdAction extends DocumentContentAction {
|
export interface SetNoteIdAction extends DocumentContentAction {
|
||||||
noteId: string
|
noteId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SetDocumentMetadataAction extends DocumentContentAction {
|
||||||
|
metadata: YAMLMetaData
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue