mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-29 07:24:28 -05:00
Add read-only view (/s/note) (#563)
* Update Link classes to allow tooltips/titles * Added read-only-view, Move note title extraction into separate file (cherry picked from commit be23083ca3966f26b1b841d5cf4f21e299c8a55a) (cherry picked from commit cbc595d3fc336b0a649c396dfae30fa08082384c) * Optimized look of document-infobar (cherry picked from commit 0176668b156da3fd7c534161a839ca0e3495119c) # Conflicts: # src/components/editor/document-bar/document-info/document-info-time-line.tsx * Show help-button only in Editor-variant of AppBar (cherry picked from commit 3c26e1619c774fe162cb3d8fae9e79ced92c9c3e) * Update CHANGELOG (cherry picked from commit d0d29e7d408515cc8f86df45d13fff60d741873e) * Move motd-banner to top of page (cherry picked from commit 43a9a274bf5da3fdf640ec905ab38153c81b014b) * Refactor isInline to size property (cherry picked from commit cb4ee74b7c97ec9711946f28924e9c890b752ea3) # Conflicts: # src/components/editor/document-bar/document-info/document-info-time-line.tsx * Add size attribute to user-avatar (cherry picked from commit 9629b58911b9d4f3aed81ef8c271fbc8e5a15aa4) * Add mode-enum to app-bar (cherry picked from commit 08f95be58974468c1e2897b475e5e3235b79c230) * Split DocumentRenderPane into scrollable- and non-scrollable variant (cherry picked from commit 44dd27edfd967745c548f7ae1fd2047e812cdc22) * Removed unnecessary className
This commit is contained in:
parent
36679753d7
commit
9e9108ec9a
23 changed files with 379 additions and 143 deletions
|
@ -32,9 +32,9 @@
|
||||||
- Highlighted code blocks can now use line wrapping and line numbers at once
|
- Highlighted code blocks can now use line wrapping and line numbers at once
|
||||||
- Images, videos, and other non-text content is now wider in View Mode
|
- Images, videos, and other non-text content is now wider in View Mode
|
||||||
- Notes may now be deleted directly from the history page
|
- Notes may now be deleted directly from the history page
|
||||||
- HedgeDoc instances can now be branded either with a '@ <custom string>' or '@ <custom logo>' after the HedgeDoc logo and text
|
- HedgeDoc instances can be branded either with a '@ \<custom string\>' or '@ \<custom logo\>' after the HedgeDoc logo and text
|
||||||
- Images will be loaded via proxy if an image proxy is configured in the backend
|
- Images will be loaded via proxy if an image proxy is configured in the backend
|
||||||
- Asciinema videos may now be embedded by pasting the URL of one video into a single line
|
- Asciinema videos may be embedded by pasting the URL of one video into a single line
|
||||||
- The toolbar includes an emoji and fork-awesome icon picker.
|
- The toolbar includes an emoji and fork-awesome icon picker.
|
||||||
- Collapsable blocks can be added via a toolbar button or via autocompletion of "<details"
|
- Collapsable blocks can be added via a toolbar button or via autocompletion of "<details"
|
||||||
- Added shortcodes for [fork-awesome icons](https://forkaweso.me/Fork-Awesome/icons/) (e.g. `:fa-picture-o:`)
|
- Added shortcodes for [fork-awesome icons](https://forkaweso.me/Fork-Awesome/icons/) (e.g. `:fa-picture-o:`)
|
||||||
|
@ -54,10 +54,11 @@
|
||||||
- The gist and pdf embeddings now use a one-click aproach similar to vimeo and youtube
|
- The gist and pdf embeddings now use a one-click aproach similar to vimeo and youtube
|
||||||
- Use [Twemoji](https://twemoji.twitter.com/) as icon font
|
- Use [Twemoji](https://twemoji.twitter.com/) as icon font
|
||||||
- The `[name=...]`, `[time=...]` and `[color=...]` tags may now be used anywhere in the document and not just inside of blockquotes and lists.
|
- The `[name=...]`, `[time=...]` and `[color=...]` tags may now be used anywhere in the document and not just inside of blockquotes and lists.
|
||||||
- The <i class="fa fa-picture-o"/> (add image) and <i class="fa fa-link"/> (add link) toolbar buttons, put selected links directly in the `()` instead of the `[]` part of the generated markdown
|
- The <i class="fa fa-picture-o"/> (add image) and <i class="fa fa-link"/> (add link) toolbar buttons put selected links directly in the `()` instead of the `[]` part of the generated markdown.
|
||||||
- The help dialog has multiple tabs, and is a bit more organized.
|
- The help dialog has multiple tabs, and is a bit more organized.
|
||||||
- Use KaTeX instead of MathJax. ([Why?](https://community.codimd.org/t/frequently-asked-questions/190))
|
- Use KaTeX instead of MathJax. ([Why?](https://community.codimd.org/t/frequently-asked-questions/190))
|
||||||
- The access tokens for the CLI and 3rd-party-clients can be managed in the user profile.
|
- The dark-mode is also applied to the read-only-view and can be toggled from there.
|
||||||
|
- Access tokens for the CLI and 3rd-party-clients can be managed in the user profile.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,11 @@
|
||||||
"id": "ABC123",
|
"id": "ABC123",
|
||||||
"alias": "banner",
|
"alias": "banner",
|
||||||
"lastChange": {
|
"lastChange": {
|
||||||
"userId": "snskxnksnxksnxksnx",
|
"userId": "test",
|
||||||
"username": "testy",
|
"timestamp": 1600033920
|
||||||
"timestamp": 123456789
|
|
||||||
},
|
},
|
||||||
"viewcount": 0,
|
"viewcount": 0,
|
||||||
"createtime": "2020-05-22T20:46:08.962Z",
|
"createtime": 1600033920,
|
||||||
"content": "This is the test banner text",
|
"content": "This is the test banner text",
|
||||||
"authorship": [],
|
"authorship": [],
|
||||||
"preVersionTwoNote": true
|
"preVersionTwoNote": true
|
||||||
|
|
|
@ -2,12 +2,11 @@
|
||||||
"id": "ABC123",
|
"id": "ABC123",
|
||||||
"alias": "old",
|
"alias": "old",
|
||||||
"lastChange": {
|
"lastChange": {
|
||||||
"userId": "snskxnksnxksnxksnx",
|
"userId": "test",
|
||||||
"username": "testy",
|
"timestamp": 1600033920
|
||||||
"timestamp": 123456789
|
|
||||||
},
|
},
|
||||||
"viewcount": 0,
|
"viewcount": 0,
|
||||||
"createtime": "2020-05-22T20:46:08.962Z",
|
"createtime": 1600033920,
|
||||||
"content": "test123",
|
"content": "test123",
|
||||||
"authorship": [],
|
"authorship": [],
|
||||||
"preVersionTwoNote": false
|
"preVersionTwoNote": false
|
||||||
|
|
|
@ -393,6 +393,18 @@
|
||||||
"clickToLoad": "Click to load"
|
"clickToLoad": "Click to load"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"views": {
|
||||||
|
"presentation": {},
|
||||||
|
"readOnly": {
|
||||||
|
"viewCount": "views",
|
||||||
|
"editNote": "Edit this note",
|
||||||
|
"loading": "Loading note contents ...",
|
||||||
|
"error": {
|
||||||
|
"title": "Error while loading note",
|
||||||
|
"description": "Probably the requested note does not exist or was deleted."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
|
||||||
|
|
||||||
interface LastChange {
|
interface LastChange {
|
||||||
userId: string
|
userId: string
|
||||||
username: string
|
|
||||||
timestamp: number
|
timestamp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,14 +10,16 @@ export interface Note {
|
||||||
alias: string
|
alias: string
|
||||||
lastChange: LastChange
|
lastChange: LastChange
|
||||||
viewcount: number
|
viewcount: number
|
||||||
createtime: string
|
createtime: number
|
||||||
content: string
|
content: string
|
||||||
authorship: number[]
|
authorship: number[]
|
||||||
preVersionTwoNote: boolean
|
preVersionTwoNote: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNote = async (noteId: string): Promise<Note> => {
|
export const getNote = async (noteId: string): Promise<Note> => {
|
||||||
const response = await fetch(getApiUrl() + `/notes/${noteId}`)
|
const response = await fetch(getApiUrl() + `/notes/${noteId}`, {
|
||||||
|
...defaultFetchConfig
|
||||||
|
})
|
||||||
expectResponseCode(response)
|
expectResponseCode(response)
|
||||||
return await response.json() as Promise<Note>
|
return await response.json() as Promise<Note>
|
||||||
}
|
}
|
||||||
|
|
11
src/components/common/document-title/note-title-extractor.ts
Normal file
11
src/components/common/document-title/note-title-extractor.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { YAMLMetaData } from '../../editor/yaml-metadata/yaml-metadata'
|
||||||
|
|
||||||
|
export const extractNoteTitle = (defaultTitle: string, noteMetadata?: YAMLMetaData, firstHeading?: string): string => {
|
||||||
|
if (noteMetadata?.title && noteMetadata?.title !== '') {
|
||||||
|
return noteMetadata.title
|
||||||
|
} else if (noteMetadata?.opengraph && noteMetadata?.opengraph.get('title') && noteMetadata?.opengraph.get('title') !== '') {
|
||||||
|
return (noteMetadata?.opengraph.get('title') ?? defaultTitle)
|
||||||
|
} else {
|
||||||
|
return (firstHeading ?? defaultTitle).trim()
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,13 +4,14 @@ import { IconName } from '../fork-awesome/types'
|
||||||
import { ShowIf } from '../show-if/show-if'
|
import { ShowIf } from '../show-if/show-if'
|
||||||
import { LinkWithTextProps } from './types'
|
import { LinkWithTextProps } from './types'
|
||||||
|
|
||||||
export const ExternalLink: React.FC<LinkWithTextProps> = ({ href, text, icon, id, className = 'text-light' }) => {
|
export const ExternalLink: React.FC<LinkWithTextProps> = ({ href, text, icon, id, className = 'text-light', title }) => {
|
||||||
return (
|
return (
|
||||||
<a href={href}
|
<a href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
id={id}
|
id={id}
|
||||||
className={className}
|
className={className}
|
||||||
|
title={title}
|
||||||
dir='auto'
|
dir='auto'
|
||||||
>
|
>
|
||||||
<ShowIf condition={!!icon}>
|
<ShowIf condition={!!icon}>
|
||||||
|
|
|
@ -5,11 +5,12 @@ import { IconName } from '../fork-awesome/types'
|
||||||
import { ShowIf } from '../show-if/show-if'
|
import { ShowIf } from '../show-if/show-if'
|
||||||
import { LinkWithTextProps } from './types'
|
import { LinkWithTextProps } from './types'
|
||||||
|
|
||||||
export const InternalLink: React.FC<LinkWithTextProps> = ({ href, text, icon, id, className = 'text-light' }) => {
|
export const InternalLink: React.FC<LinkWithTextProps> = ({ href, text, icon, id, className = 'text-light', title }) => {
|
||||||
return (
|
return (
|
||||||
<Link to={href}
|
<Link to={href}
|
||||||
className={className}
|
className={className}
|
||||||
id={id}
|
id={id}
|
||||||
|
title={title}
|
||||||
>
|
>
|
||||||
<ShowIf condition={!!icon}>
|
<ShowIf condition={!!icon}>
|
||||||
<ForkAwesomeIcon icon={icon as IconName} fixedWidth={true}/>
|
<ForkAwesomeIcon icon={icon as IconName} fixedWidth={true}/>
|
||||||
|
|
1
src/components/common/links/types.d.ts
vendored
1
src/components/common/links/types.d.ts
vendored
|
@ -6,6 +6,7 @@ interface GeneralLinkProp {
|
||||||
icon?: IconName
|
icon?: IconName
|
||||||
id?: string
|
id?: string
|
||||||
className?: string
|
className?: string
|
||||||
|
title?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LinkWithTextProps extends GeneralLinkProp {
|
export interface LinkWithTextProps extends GeneralLinkProp {
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
|
||||||
|
|
||||||
.user-name {
|
&.lg {
|
||||||
font-size: 1rem;
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sm {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,25 +4,26 @@ import { ShowIf } from '../show-if/show-if'
|
||||||
import './user-avatar.scss'
|
import './user-avatar.scss'
|
||||||
|
|
||||||
export interface UserAvatarProps {
|
export interface UserAvatarProps {
|
||||||
name: string;
|
size?: 'sm' | 'lg'
|
||||||
photo: string;
|
name: string;
|
||||||
additionalClasses?: string;
|
photo: string;
|
||||||
showName?: boolean
|
additionalClasses?: string;
|
||||||
|
showName?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserAvatar: React.FC<UserAvatarProps> = ({ name, photo, additionalClasses = '', showName = true }) => {
|
const UserAvatar: React.FC<UserAvatarProps> = ({ name, photo, size, additionalClasses = '', showName = true }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={'d-inline-flex align-items-center ' + additionalClasses}>
|
<span className={'d-inline-flex align-items-center ' + additionalClasses}>
|
||||||
<img
|
<img
|
||||||
src={photo}
|
src={photo}
|
||||||
className="user-avatar rounded"
|
className={`user-avatar rounded mr-1 ${size ?? ''}`}
|
||||||
alt={t('common.avatarOf', { name })}
|
alt={t('common.avatarOf', { name })}
|
||||||
title={name}
|
title={name}
|
||||||
/>
|
/>
|
||||||
<ShowIf condition={showName}>
|
<ShowIf condition={showName}>
|
||||||
<span className="mx-1 user-name">{name}</span>
|
<span className="mx-1">{name}</span>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,7 +16,16 @@ import { HelpButton } from './help-button/help-button'
|
||||||
import { NavbarBranding } from './navbar-branding'
|
import { NavbarBranding } from './navbar-branding'
|
||||||
import { SyncScrollButtons } from './sync-scroll-buttons/sync-scroll-buttons'
|
import { SyncScrollButtons } from './sync-scroll-buttons/sync-scroll-buttons'
|
||||||
|
|
||||||
export const AppBar: React.FC = () => {
|
export enum AppBarMode {
|
||||||
|
BASIC,
|
||||||
|
EDITOR
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppBarProps {
|
||||||
|
mode: AppBarMode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppBar: React.FC<AppBarProps> = ({ mode }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { id } = useParams<EditorPathParams>()
|
const { id } = useParams<EditorPathParams>()
|
||||||
const userExists = useSelector((state: ApplicationState) => !!state.user)
|
const userExists = useSelector((state: ApplicationState) => !!state.user)
|
||||||
|
@ -25,15 +34,19 @@ export const AppBar: React.FC = () => {
|
||||||
<Navbar bg={'light'}>
|
<Navbar bg={'light'}>
|
||||||
<Nav className="mr-auto d-flex align-items-center">
|
<Nav className="mr-auto d-flex align-items-center">
|
||||||
<NavbarBranding/>
|
<NavbarBranding/>
|
||||||
<EditorViewMode/>
|
<ShowIf condition={mode === AppBarMode.EDITOR}>
|
||||||
<SyncScrollButtons/>
|
<EditorViewMode/>
|
||||||
|
<SyncScrollButtons/>
|
||||||
|
</ShowIf>
|
||||||
<DarkModeButton/>
|
<DarkModeButton/>
|
||||||
<Link to={`/p/${id}`} target='_blank'>
|
<Link to={`/p/${id}`} target='_blank'>
|
||||||
<Button title={t('editor.documentBar.slideMode')} className="ml-2 text-secondary" size="sm" variant="outline-light">
|
<Button title={t('editor.documentBar.slideMode')} className="ml-2 text-secondary" size="sm" variant="outline-light">
|
||||||
<ForkAwesomeIcon icon="television"/>
|
<ForkAwesomeIcon icon="television"/>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<HelpButton/>
|
<ShowIf condition={mode === AppBarMode.EDITOR}>
|
||||||
|
<HelpButton/>
|
||||||
|
</ShowIf>
|
||||||
</Nav>
|
</Nav>
|
||||||
<Nav className="d-flex align-items-center text-secondary">
|
<Nav className="d-flex align-items-center text-secondary">
|
||||||
<Button className="mx-2" size="sm" variant="primary">
|
<Button className="mx-2" size="sm" variant="primary">
|
||||||
|
|
|
@ -24,6 +24,7 @@ export const DocumentInfoButton: React.FC = () => {
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
<ListGroup.Item>
|
<ListGroup.Item>
|
||||||
<DocumentInfoTimeLine
|
<DocumentInfoTimeLine
|
||||||
|
size={'2x'}
|
||||||
mode={DocumentInfoLineWithTimeMode.CREATED}
|
mode={DocumentInfoLineWithTimeMode.CREATED}
|
||||||
time={DateTime.local().minus({ days: 11 })}
|
time={DateTime.local().minus({ days: 11 })}
|
||||||
userName={'Tilman'}
|
userName={'Tilman'}
|
||||||
|
@ -31,20 +32,21 @@ export const DocumentInfoButton: React.FC = () => {
|
||||||
</ListGroup.Item>
|
</ListGroup.Item>
|
||||||
<ListGroup.Item>
|
<ListGroup.Item>
|
||||||
<DocumentInfoTimeLine
|
<DocumentInfoTimeLine
|
||||||
|
size={'2x'}
|
||||||
mode={DocumentInfoLineWithTimeMode.EDITED}
|
mode={DocumentInfoLineWithTimeMode.EDITED}
|
||||||
time={DateTime.local().minus({ minutes: 3 })}
|
time={DateTime.local().minus({ minutes: 3 })}
|
||||||
userName={'Philip'}
|
userName={'Philip'}
|
||||||
profileImageSrc={'/avatar.png'}/>
|
profileImageSrc={'/avatar.png'}/>
|
||||||
</ListGroup.Item>
|
</ListGroup.Item>
|
||||||
<ListGroup.Item>
|
<ListGroup.Item>
|
||||||
<DocumentInfoLine icon={'users'}>
|
<DocumentInfoLine icon={'users'} size={'2x'}>
|
||||||
<Trans i18nKey='editor.modal.documentInfo.usersContributed'>
|
<Trans i18nKey='editor.modal.documentInfo.usersContributed'>
|
||||||
<UnitalicBoldText text={'42'}/>
|
<UnitalicBoldText text={'42'}/>
|
||||||
</Trans>
|
</Trans>
|
||||||
</DocumentInfoLine>
|
</DocumentInfoLine>
|
||||||
</ListGroup.Item>
|
</ListGroup.Item>
|
||||||
<ListGroup.Item>
|
<ListGroup.Item>
|
||||||
<DocumentInfoLine icon={'history'}>
|
<DocumentInfoLine icon={'history'} size={'2x'}>
|
||||||
<Trans i18nKey='editor.modal.documentInfo.revisions'>
|
<Trans i18nKey='editor.modal.documentInfo.revisions'>
|
||||||
<UnitalicBoldText text={'192'}/>
|
<UnitalicBoldText text={'192'}/>
|
||||||
</Trans>
|
</Trans>
|
||||||
|
|
|
@ -4,12 +4,13 @@ import { IconName } from '../../../common/fork-awesome/types'
|
||||||
|
|
||||||
export interface DocumentInfoLineProps {
|
export interface DocumentInfoLineProps {
|
||||||
icon: IconName
|
icon: IconName
|
||||||
|
size?: '2x' | '3x' | '4x' | '5x' | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocumentInfoLine: React.FC<DocumentInfoLineProps> = ({ icon, children }) => {
|
export const DocumentInfoLine: React.FC<DocumentInfoLineProps> = ({ icon, size, children }) => {
|
||||||
return (
|
return (
|
||||||
<span className={'d-flex align-items-center'}>
|
<span className={'d-flex align-items-center'}>
|
||||||
<ForkAwesomeIcon icon={icon} size={'2x'} fixedWidth={true} className={'mx-2'}/>
|
<ForkAwesomeIcon icon={icon} size={size} fixedWidth={true} className={'mx-2'}/>
|
||||||
<i className={'d-flex align-items-center'}>
|
<i className={'d-flex align-items-center'}>
|
||||||
{children}
|
{children}
|
||||||
</i>
|
</i>
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
.document-info-avatar img {
|
|
||||||
height: 30px;
|
|
||||||
width: 30px;
|
|
||||||
}
|
|
|
@ -3,11 +3,11 @@ import React from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { IconName } from '../../../common/fork-awesome/types'
|
import { IconName } from '../../../common/fork-awesome/types'
|
||||||
import { DocumentInfoLine } from './document-info-line'
|
import { DocumentInfoLine } from './document-info-line'
|
||||||
import './document-info-time-line.scss'
|
|
||||||
import { TimeFromNow } from './time-from-now'
|
import { TimeFromNow } from './time-from-now'
|
||||||
import { UserAvatar } from '../../../common/user-avatar/user-avatar'
|
import { UserAvatar } from '../../../common/user-avatar/user-avatar'
|
||||||
|
|
||||||
export interface DocumentInfoLineWithTimeProps {
|
export interface DocumentInfoLineWithTimeProps {
|
||||||
|
size?: '2x' | '3x' | '4x' | '5x' | undefined
|
||||||
time: DateTime,
|
time: DateTime,
|
||||||
mode: DocumentInfoLineWithTimeMode
|
mode: DocumentInfoLineWithTimeMode
|
||||||
userName: string
|
userName: string
|
||||||
|
@ -19,16 +19,16 @@ export enum DocumentInfoLineWithTimeMode {
|
||||||
EDITED
|
EDITED
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocumentInfoTimeLine: React.FC<DocumentInfoLineWithTimeProps> = ({ time, mode, userName, profileImageSrc }) => {
|
export const DocumentInfoTimeLine: React.FC<DocumentInfoLineWithTimeProps> = ({ time, mode, userName, profileImageSrc, size }) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
const i18nKey = mode === DocumentInfoLineWithTimeMode.CREATED ? 'editor.modal.documentInfo.created' : 'editor.modal.documentInfo.edited'
|
const i18nKey = mode === DocumentInfoLineWithTimeMode.CREATED ? 'editor.modal.documentInfo.created' : 'editor.modal.documentInfo.edited'
|
||||||
const icon: IconName = mode === DocumentInfoLineWithTimeMode.CREATED ? 'plus' : 'pencil'
|
const icon: IconName = mode === DocumentInfoLineWithTimeMode.CREATED ? 'plus' : 'pencil'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentInfoLine icon={icon}>
|
<DocumentInfoLine icon={icon} size={size}>
|
||||||
<Trans i18nKey={i18nKey} >
|
<Trans i18nKey={i18nKey} >
|
||||||
<UserAvatar photo={profileImageSrc} additionalClasses={'document-info-avatar font-style-normal bold font-weight-bold'} name={userName}/>
|
<UserAvatar photo={profileImageSrc} additionalClasses={'font-style-normal bold font-weight-bold'} name={userName} size={size ? 'lg' : undefined}/>
|
||||||
<TimeFromNow time={time}/>
|
<TimeFromNow time={time}/>
|
||||||
</Trans>
|
</Trans>
|
||||||
</DocumentInfoLine>
|
</DocumentInfoLine>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
import React, { RefObject, useState } from 'react'
|
||||||
import { Dropdown } from 'react-bootstrap'
|
import { Dropdown } from 'react-bootstrap'
|
||||||
import useResizeObserver from 'use-resize-observer'
|
import useResizeObserver from 'use-resize-observer'
|
||||||
import { TocAst } from '../../../external-types/markdown-it-toc-done-right/interface'
|
import { TocAst } from '../../../external-types/markdown-it-toc-done-right/interface'
|
||||||
|
@ -6,119 +6,48 @@ import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { LineMarkerPosition } from '../../markdown-renderer/types'
|
import { LineMarkerPosition } from '../../markdown-renderer/types'
|
||||||
import { FullMarkdownRenderer } from '../../markdown-renderer/full-markdown-renderer'
|
import { FullMarkdownRenderer } from '../../markdown-renderer/full-markdown-renderer'
|
||||||
import { ScrollProps, ScrollState } from '../scroll/scroll-props'
|
|
||||||
import { findLineMarks } from '../scroll/utils'
|
|
||||||
import { TableOfContents } from '../table-of-contents/table-of-contents'
|
import { TableOfContents } from '../table-of-contents/table-of-contents'
|
||||||
import { YAMLMetaData } from '../yaml-metadata/yaml-metadata'
|
import { YAMLMetaData } from '../yaml-metadata/yaml-metadata'
|
||||||
|
|
||||||
interface DocumentRenderPaneProps {
|
export interface DocumentRenderPaneProps {
|
||||||
content: string
|
content: string
|
||||||
|
extraClasses?: string
|
||||||
onFirstHeadingChange: (firstHeading: string | undefined) => void
|
onFirstHeadingChange: (firstHeading: string | undefined) => void
|
||||||
|
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||||
onMetadataChange: (metaData: YAMLMetaData | undefined) => void
|
onMetadataChange: (metaData: YAMLMetaData | undefined) => void
|
||||||
|
onMouseEnterRenderer?: () => void
|
||||||
|
onScrollRenderer?: () => void
|
||||||
onTaskCheckedChange: (lineInMarkdown: number, checked: boolean) => void
|
onTaskCheckedChange: (lineInMarkdown: number, checked: boolean) => void
|
||||||
|
rendererReference?: RefObject<HTMLDivElement>
|
||||||
wide?: boolean
|
wide?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocumentRenderPane: React.FC<DocumentRenderPaneProps & ScrollProps> = ({
|
export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = ({
|
||||||
content,
|
content,
|
||||||
|
extraClasses,
|
||||||
onFirstHeadingChange,
|
onFirstHeadingChange,
|
||||||
onMakeScrollSource,
|
onLineMarkerPositionChanged,
|
||||||
onMetadataChange,
|
onMetadataChange,
|
||||||
onScroll,
|
onMouseEnterRenderer,
|
||||||
|
onScrollRenderer,
|
||||||
onTaskCheckedChange,
|
onTaskCheckedChange,
|
||||||
scrollState,
|
rendererReference,
|
||||||
wide
|
wide
|
||||||
}) => {
|
}) => {
|
||||||
const [tocAst, setTocAst] = useState<TocAst>()
|
const [tocAst, setTocAst] = useState<TocAst>()
|
||||||
const renderer = useRef<HTMLDivElement>(null)
|
const { width } = useResizeObserver(rendererReference ? { ref: rendererReference } : undefined)
|
||||||
const { width } = useResizeObserver({ ref: renderer })
|
|
||||||
const lastScrollPosition = useRef<number>()
|
|
||||||
const [lineMarks, setLineMarks] = useState<LineMarkerPosition[]>()
|
|
||||||
|
|
||||||
const realWidth = width || 0
|
const realWidth = width || 0
|
||||||
|
|
||||||
const scrollTo = useCallback((targetPosition:number):void => {
|
|
||||||
if (!renderer.current || targetPosition === lastScrollPosition.current) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lastScrollPosition.current = targetPosition
|
|
||||||
renderer.current.scrollTo({
|
|
||||||
top: targetPosition
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!renderer.current || !lineMarks || lineMarks.length === 0 || !scrollState) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (scrollState.firstLineInView < lineMarks[0].line) {
|
|
||||||
scrollTo(0)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (scrollState.firstLineInView > lineMarks[lineMarks.length - 1].line) {
|
|
||||||
scrollTo(renderer.current.offsetHeight)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const { lastMarkBefore, firstMarkAfter } = findLineMarks(lineMarks, scrollState.firstLineInView)
|
|
||||||
const positionBefore = lastMarkBefore ? lastMarkBefore.position : lineMarks[0].position
|
|
||||||
const positionAfter = firstMarkAfter ? firstMarkAfter.position : renderer.current.offsetHeight
|
|
||||||
const lastMarkBeforeLine = lastMarkBefore ? lastMarkBefore.line : 1
|
|
||||||
const firstMarkAfterLine = firstMarkAfter ? firstMarkAfter.line : content.split('\n').length
|
|
||||||
const lineCount = firstMarkAfterLine - lastMarkBeforeLine
|
|
||||||
const blockHeight = positionAfter - positionBefore
|
|
||||||
const lineHeight = blockHeight / lineCount
|
|
||||||
const position = positionBefore + (scrollState.firstLineInView - lastMarkBeforeLine) * lineHeight + scrollState.scrolledPercentage / 100 * lineHeight
|
|
||||||
const correctedPosition = Math.floor(position)
|
|
||||||
scrollTo(correctedPosition)
|
|
||||||
}, [content, lineMarks, scrollState, scrollTo])
|
|
||||||
|
|
||||||
const userScroll = useCallback(() => {
|
|
||||||
if (!renderer.current || !lineMarks || lineMarks.length === 0 || !onScroll) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollTop = renderer.current.scrollTop
|
|
||||||
|
|
||||||
const lineMarksBeforeScrollTop = lineMarks.filter(lineMark => lineMark.position <= scrollTop)
|
|
||||||
if (lineMarksBeforeScrollTop.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const lineMarksAfterScrollTop = lineMarks.filter(lineMark => lineMark.position > scrollTop)
|
|
||||||
if (lineMarksAfterScrollTop.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const beforeLineMark = lineMarksBeforeScrollTop
|
|
||||||
.reduce((prevLineMark, currentLineMark) =>
|
|
||||||
prevLineMark.line >= currentLineMark.line ? prevLineMark : currentLineMark)
|
|
||||||
|
|
||||||
const afterLineMark = lineMarksAfterScrollTop
|
|
||||||
.reduce((prevLineMark, currentLineMark) =>
|
|
||||||
prevLineMark.line < currentLineMark.line ? prevLineMark : currentLineMark)
|
|
||||||
|
|
||||||
const componentHeight = afterLineMark.position - beforeLineMark.position
|
|
||||||
const distanceToBefore = scrollTop - beforeLineMark.position
|
|
||||||
const percentageRaw = (distanceToBefore / componentHeight)
|
|
||||||
const lineCount = afterLineMark.line - beforeLineMark.line
|
|
||||||
const line = Math.floor(lineCount * percentageRaw + beforeLineMark.line)
|
|
||||||
const lineHeight = componentHeight / lineCount
|
|
||||||
const innerScrolling = Math.floor((distanceToBefore % lineHeight) / lineHeight * 100)
|
|
||||||
|
|
||||||
const newScrollState: ScrollState = { firstLineInView: line, scrolledPercentage: innerScrolling }
|
|
||||||
onScroll(newScrollState)
|
|
||||||
}, [lineMarks, onScroll])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'bg-light flex-fill pb-5 flex-row d-flex w-100 h-100 overflow-y-scroll'}
|
<div className={`bg-light flex-fill pb-5 flex-row d-flex w-100 h-100 ${extraClasses ?? ''}`}
|
||||||
ref={renderer} onScroll={userScroll} onMouseEnter={onMakeScrollSource}>
|
ref={rendererReference} onScroll={onScrollRenderer} onMouseEnter={onMouseEnterRenderer}>
|
||||||
<div className={'col-md'}/>
|
<div className={'col-md'}/>
|
||||||
<div className={'bg-light flex-fill'}>
|
<div className={'bg-light flex-fill'}>
|
||||||
<FullMarkdownRenderer
|
<FullMarkdownRenderer
|
||||||
className={'flex-fill mb-3'}
|
className={'flex-fill mb-3'}
|
||||||
content={content}
|
content={content}
|
||||||
onFirstHeadingChange={onFirstHeadingChange}
|
onFirstHeadingChange={onFirstHeadingChange}
|
||||||
onLineMarkerPositionChanged={setLineMarks}
|
onLineMarkerPositionChanged={onLineMarkerPositionChanged}
|
||||||
onMetaDataChange={onMetadataChange}
|
onMetaDataChange={onMetadataChange}
|
||||||
onTaskCheckedChange={onTaskCheckedChange}
|
onTaskCheckedChange={onTaskCheckedChange}
|
||||||
onTocChange={(tocAst) => setTocAst(tocAst)}
|
onTocChange={(tocAst) => setTocAst(tocAst)}
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { LineMarkerPosition } from '../../markdown-renderer/types'
|
||||||
|
import { ScrollProps, ScrollState } from '../scroll/scroll-props'
|
||||||
|
import { findLineMarks } from '../scroll/utils'
|
||||||
|
import { DocumentRenderPane, DocumentRenderPaneProps } from './document-render-pane'
|
||||||
|
|
||||||
|
export const ScrollingDocumentRenderPane: React.FC<DocumentRenderPaneProps & ScrollProps> = ({
|
||||||
|
content,
|
||||||
|
scrollState,
|
||||||
|
wide,
|
||||||
|
onFirstHeadingChange,
|
||||||
|
onMakeScrollSource,
|
||||||
|
onMetadataChange,
|
||||||
|
onScroll,
|
||||||
|
onTaskCheckedChange
|
||||||
|
}) => {
|
||||||
|
const lastScrollPosition = useRef<number>()
|
||||||
|
const renderer = useRef<HTMLDivElement>(null)
|
||||||
|
const [lineMarks, setLineMarks] = useState<LineMarkerPosition[]>()
|
||||||
|
|
||||||
|
const scrollTo = useCallback((targetPosition: number): void => {
|
||||||
|
if (!renderer.current || targetPosition === lastScrollPosition.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastScrollPosition.current = targetPosition
|
||||||
|
renderer.current.scrollTo({
|
||||||
|
top: targetPosition
|
||||||
|
})
|
||||||
|
}, [renderer])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!renderer.current || !lineMarks || lineMarks.length === 0 || !scrollState) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (scrollState.firstLineInView < lineMarks[0].line) {
|
||||||
|
scrollTo(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (scrollState.firstLineInView > lineMarks[lineMarks.length - 1].line) {
|
||||||
|
scrollTo(renderer.current.offsetHeight)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { lastMarkBefore, firstMarkAfter } = findLineMarks(lineMarks, scrollState.firstLineInView)
|
||||||
|
const positionBefore = lastMarkBefore ? lastMarkBefore.position : lineMarks[0].position
|
||||||
|
const positionAfter = firstMarkAfter ? firstMarkAfter.position : renderer.current.offsetHeight
|
||||||
|
const lastMarkBeforeLine = lastMarkBefore ? lastMarkBefore.line : 1
|
||||||
|
const firstMarkAfterLine = firstMarkAfter ? firstMarkAfter.line : content.split('\n').length
|
||||||
|
const lineCount = firstMarkAfterLine - lastMarkBeforeLine
|
||||||
|
const blockHeight = positionAfter - positionBefore
|
||||||
|
const lineHeight = blockHeight / lineCount
|
||||||
|
const position = positionBefore + (scrollState.firstLineInView - lastMarkBeforeLine) * lineHeight + scrollState.scrolledPercentage / 100 * lineHeight
|
||||||
|
const correctedPosition = Math.floor(position)
|
||||||
|
scrollTo(correctedPosition)
|
||||||
|
}, [content, lineMarks, scrollState, scrollTo])
|
||||||
|
|
||||||
|
const userScroll = useCallback(() => {
|
||||||
|
if (!renderer.current || !lineMarks || lineMarks.length === 0 || !onScroll) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollTop = renderer.current.scrollTop
|
||||||
|
|
||||||
|
const lineMarksBeforeScrollTop = lineMarks.filter(lineMark => lineMark.position <= scrollTop)
|
||||||
|
if (lineMarksBeforeScrollTop.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineMarksAfterScrollTop = lineMarks.filter(lineMark => lineMark.position > scrollTop)
|
||||||
|
if (lineMarksAfterScrollTop.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeLineMark = lineMarksBeforeScrollTop
|
||||||
|
.reduce((prevLineMark, currentLineMark) =>
|
||||||
|
prevLineMark.line >= currentLineMark.line ? prevLineMark : currentLineMark)
|
||||||
|
|
||||||
|
const afterLineMark = lineMarksAfterScrollTop
|
||||||
|
.reduce((prevLineMark, currentLineMark) =>
|
||||||
|
prevLineMark.line < currentLineMark.line ? prevLineMark : currentLineMark)
|
||||||
|
|
||||||
|
const componentHeight = afterLineMark.position - beforeLineMark.position
|
||||||
|
const distanceToBefore = scrollTop - beforeLineMark.position
|
||||||
|
const percentageRaw = (distanceToBefore / componentHeight)
|
||||||
|
const lineCount = afterLineMark.line - beforeLineMark.line
|
||||||
|
const line = Math.floor(lineCount * percentageRaw + beforeLineMark.line)
|
||||||
|
const lineHeight = componentHeight / lineCount
|
||||||
|
const innerScrolling = Math.floor((distanceToBefore % lineHeight) / lineHeight * 100)
|
||||||
|
|
||||||
|
const newScrollState: ScrollState = { firstLineInView: line, scrolledPercentage: innerScrolling }
|
||||||
|
onScroll(newScrollState)
|
||||||
|
}, [lineMarks, onScroll])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocumentRenderPane
|
||||||
|
content={content}
|
||||||
|
extraClasses={'overflow-y-scroll'}
|
||||||
|
rendererReference={renderer}
|
||||||
|
wide={wide}
|
||||||
|
onFirstHeadingChange={onFirstHeadingChange}
|
||||||
|
onLineMarkerPositionChanged={setLineMarks}
|
||||||
|
onMetadataChange={onMetadataChange}
|
||||||
|
onMouseEnterRenderer={onMakeScrollSource}
|
||||||
|
onScrollRenderer={userScroll}
|
||||||
|
onTaskCheckedChange={onTaskCheckedChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -6,11 +6,12 @@ import { ApplicationState } from '../../redux'
|
||||||
import { setEditorMode } from '../../redux/editor/methods'
|
import { setEditorMode } from '../../redux/editor/methods'
|
||||||
import { ApplyDarkMode } from '../common/apply-dark-mode/apply-dark-mode'
|
import { ApplyDarkMode } from '../common/apply-dark-mode/apply-dark-mode'
|
||||||
import { DocumentTitle } from '../common/document-title/document-title'
|
import { DocumentTitle } from '../common/document-title/document-title'
|
||||||
|
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 { AppBar } from './app-bar/app-bar'
|
import { AppBar, AppBarMode } from './app-bar/app-bar'
|
||||||
import { EditorMode } from './app-bar/editor-view-mode'
|
import { EditorMode } from './app-bar/editor-view-mode'
|
||||||
import { DocumentBar } from './document-bar/document-bar'
|
import { DocumentBar } from './document-bar/document-bar'
|
||||||
import { DocumentRenderPane } from './document-renderer-pane/document-render-pane'
|
import { ScrollingDocumentRenderPane } from './document-renderer-pane/scrolling-document-render-pane'
|
||||||
import { EditorPane } from './editor-pane/editor-pane'
|
import { EditorPane } from './editor-pane/editor-pane'
|
||||||
import { editorTestContent } from './editorTestContent'
|
import { editorTestContent } from './editorTestContent'
|
||||||
import { DualScrollState, ScrollState } from './scroll/scroll-props'
|
import { DualScrollState, ScrollState } from './scroll/scroll-props'
|
||||||
|
@ -49,14 +50,9 @@ export const Editor: React.FC = () => {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const updateDocumentTitle = useCallback(() => {
|
const updateDocumentTitle = useCallback(() => {
|
||||||
if (noteMetadata.current?.title && noteMetadata.current?.title !== '') {
|
const noteTitle = extractNoteTitle(untitledNote, noteMetadata.current, firstHeading.current)
|
||||||
setDocumentTitle(noteMetadata.current.title)
|
setDocumentTitle(noteTitle)
|
||||||
} else if (noteMetadata.current?.opengraph && noteMetadata.current?.opengraph.get('title') && noteMetadata.current?.opengraph.get('title') !== '') {
|
}, [noteMetadata, firstHeading, untitledNote])
|
||||||
setDocumentTitle(noteMetadata.current.opengraph.get('title') ?? untitledNote)
|
|
||||||
} else {
|
|
||||||
setDocumentTitle((firstHeading.current ?? untitledNote).trim())
|
|
||||||
}
|
|
||||||
}, [untitledNote])
|
|
||||||
|
|
||||||
const onFirstHeadingChange = useCallback((newFirstHeading: string | undefined) => {
|
const onFirstHeadingChange = useCallback((newFirstHeading: string | undefined) => {
|
||||||
firstHeading.current = newFirstHeading
|
firstHeading.current = newFirstHeading
|
||||||
|
@ -114,7 +110,7 @@ export const Editor: React.FC = () => {
|
||||||
<MotdBanner/>
|
<MotdBanner/>
|
||||||
<DocumentTitle title={documentTitle}/>
|
<DocumentTitle title={documentTitle}/>
|
||||||
<div className={'d-flex flex-column vh-100'}>
|
<div className={'d-flex flex-column vh-100'}>
|
||||||
<AppBar/>
|
<AppBar mode={AppBarMode.EDITOR}/>
|
||||||
<DocumentBar title={documentTitle} noteContent={markdownContent} updateNoteContent={(newContent) => setMarkdownContent(newContent)}/>
|
<DocumentBar title={documentTitle} noteContent={markdownContent} updateNoteContent={(newContent) => setMarkdownContent(newContent)}/>
|
||||||
<Splitter
|
<Splitter
|
||||||
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH}
|
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH}
|
||||||
|
@ -129,7 +125,7 @@ export const Editor: React.FC = () => {
|
||||||
}
|
}
|
||||||
showRight={editorMode === EditorMode.PREVIEW || (editorMode === EditorMode.BOTH)}
|
showRight={editorMode === EditorMode.PREVIEW || (editorMode === EditorMode.BOTH)}
|
||||||
right={
|
right={
|
||||||
<DocumentRenderPane
|
<ScrollingDocumentRenderPane
|
||||||
content={markdownContent}
|
content={markdownContent}
|
||||||
onFirstHeadingChange={onFirstHeadingChange}
|
onFirstHeadingChange={onFirstHeadingChange}
|
||||||
onMakeScrollSource={() => { scrollSource.current = ScrollSource.RENDERER }}
|
onMakeScrollSource={() => { scrollSource.current = ScrollSource.RENDERER }}
|
||||||
|
|
4
src/components/pad-view-only/document-infobar.scss
Normal file
4
src/components/pad-view-only/document-infobar.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.document-infobar {
|
||||||
|
color: #777;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
60
src/components/pad-view-only/document-infobar.tsx
Normal file
60
src/components/pad-view-only/document-infobar.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
import React from 'react'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { InternalLink } from '../common/links/internal-link'
|
||||||
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
|
import './document-infobar.scss'
|
||||||
|
import {
|
||||||
|
DocumentInfoLineWithTimeMode,
|
||||||
|
DocumentInfoTimeLine
|
||||||
|
} from '../editor/document-bar/document-info/document-info-time-line'
|
||||||
|
|
||||||
|
export interface DocumentInfobarProps {
|
||||||
|
changedAuthor: string
|
||||||
|
changedTime: number
|
||||||
|
createdAuthor: string
|
||||||
|
createdTime: number
|
||||||
|
editable: boolean
|
||||||
|
noteId: string
|
||||||
|
viewCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DocumentInfobar: React.FC<DocumentInfobarProps> = ({
|
||||||
|
changedAuthor,
|
||||||
|
changedTime,
|
||||||
|
createdAuthor,
|
||||||
|
createdTime,
|
||||||
|
editable,
|
||||||
|
noteId,
|
||||||
|
viewCount
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'d-flex flex-row my-3 document-infobar'}>
|
||||||
|
<div className={'col-md'}> </div>
|
||||||
|
<div className={'d-flex flex-fill'}>
|
||||||
|
<div className={'d-flex flex-column'}>
|
||||||
|
<DocumentInfoTimeLine
|
||||||
|
mode={DocumentInfoLineWithTimeMode.CREATED}
|
||||||
|
time={ DateTime.fromSeconds(createdTime) }
|
||||||
|
userName={createdAuthor}
|
||||||
|
profileImageSrc={'/avatar.png'}/>
|
||||||
|
<DocumentInfoTimeLine
|
||||||
|
mode={DocumentInfoLineWithTimeMode.EDITED}
|
||||||
|
time={ DateTime.fromSeconds(changedTime) }
|
||||||
|
userName={changedAuthor}
|
||||||
|
profileImageSrc={'/avatar.png'}/>
|
||||||
|
<hr/>
|
||||||
|
</div>
|
||||||
|
<span className={'ml-auto'}>
|
||||||
|
{ viewCount } <Trans i18nKey={'views.readOnly.viewCount'}/>
|
||||||
|
<ShowIf condition={editable}>
|
||||||
|
<InternalLink text={''} href={`/n/${noteId}`} icon={'pencil'} className={'text-primary text-decoration-none mx-1'} title={t('views.readOnly.editNote')}/>
|
||||||
|
</ShowIf>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={'col-md'}> </div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
90
src/components/pad-view-only/pad-view-only.tsx
Normal file
90
src/components/pad-view-only/pad-view-only.tsx
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { Alert } from 'react-bootstrap'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { useParams } from 'react-router'
|
||||||
|
import { getNote, Note } from '../../api/notes'
|
||||||
|
import { ApplyDarkMode } from '../common/apply-dark-mode/apply-dark-mode'
|
||||||
|
import { DocumentTitle } from '../common/document-title/document-title'
|
||||||
|
import { extractNoteTitle } from '../common/document-title/note-title-extractor'
|
||||||
|
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
||||||
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
|
import { AppBar, AppBarMode } from '../editor/app-bar/app-bar'
|
||||||
|
import { DocumentRenderPane } from '../editor/document-renderer-pane/document-render-pane'
|
||||||
|
import { EditorPathParams } from '../editor/editor'
|
||||||
|
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
|
||||||
|
import { DocumentInfobar } from './document-infobar'
|
||||||
|
|
||||||
|
export const PadViewOnly: React.FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { id } = useParams<EditorPathParams>()
|
||||||
|
const untitledNote = t('editor.untitledNote')
|
||||||
|
const [documentTitle, setDocumentTitle] = useState(untitledNote)
|
||||||
|
const [noteData, setNoteData] = useState<Note>()
|
||||||
|
const [error, setError] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const noteMetadata = useRef<YAMLMetaData>()
|
||||||
|
const firstHeading = useRef<string>()
|
||||||
|
|
||||||
|
const updateDocumentTitle = useCallback(() => {
|
||||||
|
const noteTitle = extractNoteTitle(untitledNote, noteMetadata.current, firstHeading.current)
|
||||||
|
setDocumentTitle(noteTitle)
|
||||||
|
}, [untitledNote])
|
||||||
|
|
||||||
|
const onFirstHeadingChange = useCallback((newFirstHeading: string | undefined) => {
|
||||||
|
firstHeading.current = newFirstHeading
|
||||||
|
updateDocumentTitle()
|
||||||
|
}, [updateDocumentTitle])
|
||||||
|
|
||||||
|
const onMetadataChange = useCallback((metaData: YAMLMetaData | undefined) => {
|
||||||
|
noteMetadata.current = metaData
|
||||||
|
updateDocumentTitle()
|
||||||
|
}, [updateDocumentTitle])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getNote(id)
|
||||||
|
.then(note => setNoteData(note))
|
||||||
|
.catch(() => setError(true))
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [id])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'d-flex flex-column mvh-100 bg-light'}>
|
||||||
|
<ApplyDarkMode/>
|
||||||
|
<DocumentTitle title={documentTitle}/>
|
||||||
|
<MotdBanner/>
|
||||||
|
<AppBar mode={AppBarMode.BASIC}/>
|
||||||
|
<div className={'container'}>
|
||||||
|
<ShowIf condition={error}>
|
||||||
|
<Alert variant={'danger'} className={'my-2'}>
|
||||||
|
<b><Trans i18nKey={'views.readOnly.error.title'}/></b>
|
||||||
|
<br/>
|
||||||
|
<Trans i18nKey={'views.readOnly.error.description'}/>
|
||||||
|
</Alert>
|
||||||
|
</ShowIf>
|
||||||
|
<ShowIf condition={loading}>
|
||||||
|
<Alert variant={'info'} className={'my-2'}>
|
||||||
|
<Trans i18nKey={'views.readOnly.loading'}/>
|
||||||
|
</Alert>
|
||||||
|
</ShowIf>
|
||||||
|
</div>
|
||||||
|
<ShowIf condition={!error && !loading}>
|
||||||
|
{ /* TODO set editable and created author properly */ }
|
||||||
|
<DocumentInfobar
|
||||||
|
changedAuthor={noteData?.lastChange.userId ?? ''}
|
||||||
|
changedTime={noteData?.lastChange.timestamp ?? 0}
|
||||||
|
createdAuthor={'Test'}
|
||||||
|
createdTime={noteData?.createtime ?? 0}
|
||||||
|
editable={true}
|
||||||
|
noteId={id}
|
||||||
|
viewCount={noteData?.viewcount ?? 0}
|
||||||
|
/>
|
||||||
|
<DocumentRenderPane
|
||||||
|
content={noteData?.content ?? ''}
|
||||||
|
onFirstHeadingChange={onFirstHeadingChange}
|
||||||
|
onMetadataChange={onMetadataChange}
|
||||||
|
onTaskCheckedChange={() => false}
|
||||||
|
/>
|
||||||
|
</ShowIf>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import { HistoryPage } from './components/history-page/history-page'
|
||||||
import { IntroPage } from './components/intro-page/intro-page'
|
import { IntroPage } from './components/intro-page/intro-page'
|
||||||
import { LandingLayout } from './components/landing-layout/landing-layout'
|
import { LandingLayout } from './components/landing-layout/landing-layout'
|
||||||
import { LoginPage } from './components/login-page/login-page'
|
import { LoginPage } from './components/login-page/login-page'
|
||||||
|
import { PadViewOnly } from './components/pad-view-only/pad-view-only'
|
||||||
import { ProfilePage } from './components/profile-page/profile-page'
|
import { ProfilePage } from './components/profile-page/profile-page'
|
||||||
import { RegisterPage } from './components/register-page/register-page'
|
import { RegisterPage } from './components/register-page/register-page'
|
||||||
import { store } from './redux'
|
import { store } from './redux'
|
||||||
|
@ -53,6 +54,9 @@ ReactDOM.render(
|
||||||
<Route path="/n/:id">
|
<Route path="/n/:id">
|
||||||
<Editor/>
|
<Editor/>
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/s/:id">
|
||||||
|
<PadViewOnly/>
|
||||||
|
</Route>
|
||||||
<Route path="/:id">
|
<Route path="/:id">
|
||||||
<Redirector/>
|
<Redirector/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
Loading…
Reference in a new issue