2023-01-19 17:19:06 +00:00
import * as React from 'react' ;
import { themeStyle } from '@joplin/lib/theme' ;
import { _ } from '@joplin/lib/locale' ;
2025-01-13 08:33:42 -08:00
import NoteTextViewer , { NoteViewerControl } from './NoteTextViewer' ;
2023-01-19 17:19:06 +00:00
import HelpButton from './HelpButton' ;
import BaseModel from '@joplin/lib/BaseModel' ;
import Revision from '@joplin/lib/models/Revision' ;
import RevisionService from '@joplin/lib/services/RevisionService' ;
2025-01-13 08:33:42 -08:00
import { MarkupLanguage } from '@joplin/renderer' ;
2023-01-19 17:19:06 +00:00
import time from '@joplin/lib/time' ;
import bridge from '../services/bridge' ;
import { NoteEntity , RevisionEntity } from '@joplin/lib/services/database/types' ;
import { AppState } from '../app.reducer' ;
2020-11-07 15:59:37 +00:00
const urlUtils = require ( '@joplin/lib/urlUtils' ) ;
2019-05-06 21:35:29 +01:00
const ReactTooltip = require ( 'react-tooltip' ) ;
2023-01-19 17:19:06 +00:00
const { connect } = require ( 'react-redux' ) ;
2023-02-18 15:31:59 +00:00
import shared from '@joplin/lib/components/shared/note-screen-shared' ;
2025-01-08 04:30:16 -08:00
import shim , { MessageBoxType } from '@joplin/lib/shim' ;
2025-01-13 08:33:42 -08:00
import { RefObject , useCallback , useRef , useState } from 'react' ;
import useQueuedAsyncEffect from '@joplin/lib/hooks/useQueuedAsyncEffect' ;
import useMarkupToHtml from './hooks/useMarkupToHtml' ;
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect' ;
import { ScrollbarSize } from '@joplin/lib/models/settings/builtInMetadata' ;
2023-01-19 17:19:06 +00:00
interface Props {
themeId : number ;
noteId : string ;
2025-01-13 08:33:42 -08:00
onBack : ( ) = > void ;
2023-01-19 17:19:06 +00:00
customCss : string ;
2025-01-13 08:33:42 -08:00
scrollbarSize : ScrollbarSize ;
2023-01-19 17:19:06 +00:00
}
2025-01-13 08:33:42 -08:00
const useNoteContent = (
viewerRef : RefObject < NoteViewerControl > ,
currentRevId : string ,
revisions : RevisionEntity [ ] ,
themeId : number ,
customCss : string ,
scrollbarSize : ScrollbarSize ,
) = > {
const [ note , setNote ] = useState < NoteEntity > ( null ) ;
const markupToHtml = useMarkupToHtml ( {
themeId ,
customCss ,
plugins : { } ,
whiteBackgroundNoteRendering : false ,
scrollbarSize ,
} ) ;
useAsyncEffect ( async ( event ) = > {
if ( ! revisions . length || ! currentRevId ) {
setNote ( null ) ;
} else {
const revIndex = BaseModel . modelIndexById ( revisions , currentRevId ) ;
const note = await RevisionService . instance ( ) . revisionNote ( revisions , revIndex ) ;
if ( ! note || event . cancelled ) return ;
setNote ( note ) ;
}
} , [ revisions , currentRevId , themeId , customCss , viewerRef ] ) ;
2023-01-19 17:19:06 +00:00
2025-01-13 08:33:42 -08:00
useQueuedAsyncEffect ( async ( ) = > {
const noteBody = note ? . body ? ? _ ( 'This note has no history' ) ;
const markupLanguage = note . markup_language ? ? MarkupLanguage . Markdown ;
const result = await markupToHtml ( markupLanguage , noteBody , {
resources : await shared . attachedResources ( noteBody ) ,
whiteBackgroundNoteRendering : markupLanguage === MarkupLanguage . Html ,
} ) ;
2023-01-19 17:19:06 +00:00
2025-01-13 08:33:42 -08:00
viewerRef . current . setHtml ( result . html , {
pluginAssets : result.pluginAssets ,
} ) ;
} , [ note , viewerRef ] ) ;
2019-05-06 21:35:29 +01:00
2025-01-13 08:33:42 -08:00
return note ;
} ;
const NoteRevisionViewerComponent : React.FC < Props > = ( { themeId , noteId , onBack , customCss , scrollbarSize } ) = > {
const helpButton_onClick = useCallback ( ( ) = > { } , [ ] ) ;
const viewerRef = useRef < NoteViewerControl | null > ( null ) ;
const [ revisions , setRevisions ] = useState < RevisionEntity [ ] > ( [ ] ) ;
const [ currentRevId , setCurrentRevId ] = useState ( '' ) ;
const [ restoring , setRestoring ] = useState ( false ) ;
2019-05-06 21:35:29 +01:00
2025-01-13 08:33:42 -08:00
const note = useNoteContent ( viewerRef , currentRevId , revisions , themeId , customCss , scrollbarSize ) ;
const viewer_domReady = useCallback ( async ( ) = > {
2022-11-14 16:48:41 +00:00
// this.viewerRef_.current.openDevTools();
2019-05-06 21:35:29 +01:00
2025-01-13 08:33:42 -08:00
const revisions = await Revision . allByType ( BaseModel . TYPE_NOTE , noteId ) ;
2019-07-29 14:13:23 +02:00
2025-01-13 08:33:42 -08:00
setRevisions ( revisions ) ;
setCurrentRevId ( revisions . length ? revisions [ revisions . length - 1 ] . id : '' ) ;
} , [ noteId ] ) ;
2019-05-06 21:35:29 +01:00
2025-01-13 08:33:42 -08:00
const importButton_onClick = useCallback ( async ( ) = > {
if ( ! note ) return ;
setRestoring ( true ) ;
await RevisionService . instance ( ) . importRevisionNote ( note ) ;
setRestoring ( false ) ;
await shim . showMessageBox ( RevisionService . instance ( ) . restoreSuccessMessage ( note ) , { type : MessageBoxType . Info } ) ;
} , [ note ] ) ;
2019-05-06 21:35:29 +01:00
2025-01-13 08:33:42 -08:00
const backButton_click = useCallback ( ( ) = > {
if ( onBack ) onBack ( ) ;
} , [ onBack ] ) ;
2019-05-06 21:35:29 +01:00
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2025-01-13 08:33:42 -08:00
const revisionList_onChange : React.ChangeEventHandler < HTMLSelectElement > = useCallback ( ( event ) = > {
2019-05-06 21:35:29 +01:00
const value = event . target . value ;
if ( ! value ) {
2025-01-13 08:33:42 -08:00
if ( onBack ) onBack ( ) ;
2019-05-06 21:35:29 +01:00
} else {
2025-01-13 08:33:42 -08:00
setCurrentRevId ( value ) ;
2019-05-06 21:35:29 +01:00
}
2025-01-13 08:33:42 -08:00
} , [ onBack ] ) ;
2019-05-06 21:35:29 +01:00
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2025-01-13 08:33:42 -08:00
const webview_ipcMessage = useCallback ( async ( event : any ) = > {
2024-02-26 10:16:23 +00:00
// For the revision view, we only support a minimal subset of the IPC messages.
2019-10-28 18:47:23 +00:00
// For example, we don't need interactive checkboxes or sync between viewer and editor view.
// We try to get most links work though, except for internal (joplin://) links.
const msg = event . channel ? event . channel : '' ;
2020-07-23 19:56:53 +00:00
// const args = event.args;
2019-10-28 18:47:23 +00:00
2020-07-23 19:56:53 +00:00
// if (msg !== 'percentScroll') console.info(`Got ipc-message: ${msg}`, args);
2019-10-28 18:47:23 +00:00
try {
if ( msg . indexOf ( 'joplin://' ) === 0 ) {
throw new Error ( _ ( 'Unsupported link or message: %s' , msg ) ) ;
} else if ( urlUtils . urlProtocol ( msg ) ) {
2024-02-22 13:29:16 -08:00
await bridge ( ) . openExternal ( msg ) ;
2019-10-28 18:47:23 +00:00
} else if ( msg . indexOf ( '#' ) === 0 ) {
// This is an internal anchor, which is handled by the WebView so skip this case
} else {
console . warn ( ` Unsupported message in revision view: ${ msg } ` ) ;
}
} catch ( error ) {
console . warn ( error ) ;
bridge ( ) . showErrorMessageBox ( error . message ) ;
}
2025-01-13 08:33:42 -08:00
} , [ ] ) ;
2019-10-28 18:47:23 +00:00
2025-01-13 08:33:42 -08:00
const theme = themeStyle ( themeId ) ;
2019-05-06 21:35:29 +01:00
2025-01-13 08:33:42 -08:00
const revisionListItems = [ ] ;
const revs = revisions . slice ( ) . reverse ( ) ;
for ( let i = 0 ; i < revs . length ; i ++ ) {
const rev = revs [ i ] ;
const stats = Revision . revisionPatchStatsText ( rev ) ;
2019-05-06 21:35:29 +01:00
2025-01-13 08:33:42 -08:00
revisionListItems . push (
< option key = { rev . id } value = { rev . id } >
{ ` ${ time . formatMsToLocal ( rev . item_updated_time ) } ( ${ stats } ) ` }
< / option > ,
2019-05-06 21:35:29 +01:00
) ;
}
2025-01-13 08:33:42 -08:00
const restoreButtonTitle = _ ( 'Restore' ) ;
const helpMessage = _ ( 'Click "%s" to restore the note. It will be copied in the notebook named "%s". The current version of the note will not be replaced or modified.' , restoreButtonTitle , RevisionService . instance ( ) . restoreFolderTitle ( ) ) ;
const titleInput = (
< div className = 'revision-viewer-title' >
< button onClick = { backButton_click } style = { { . . . theme . buttonStyle , marginRight : 10 , height : theme.inputStyle.height } } >
< i style = { theme . buttonIconStyle } className = { 'fa fa-chevron-left' } > < / i > { _ ( 'Back' ) }
< / button >
< input readOnly type = "text" className = 'title' style = { theme . inputStyle } value = { note ? . title ? ? '' } / >
< select disabled = { ! revisions . length } value = { currentRevId } className = 'revisions' style = { theme . dropdownList } onChange = { revisionList_onChange } >
{ revisionListItems }
< / select >
< button disabled = { ! revisions . length || restoring } onClick = { importButton_onClick } className = 'restore' style = { { . . . theme . buttonStyle , marginLeft : 10 , height : theme.inputStyle.height } } >
{ restoreButtonTitle }
< / button >
< HelpButton tip = { helpMessage } id = "noteRevisionHelpButton" onClick = { helpButton_onClick } / >
< / div >
) ;
const viewer = < NoteTextViewer themeId = { themeId } viewerStyle = { { display : 'flex' , flex : 1 , borderLeft : 'none' } } ref = { viewerRef } onDomReady = { viewer_domReady } onIpcMessage = { webview_ipcMessage } / > ;
return (
< div className = 'revision-viewer-root' >
{ titleInput }
{ viewer }
< ReactTooltip place = "bottom" delayShow = { 300 } className = "help-tooltip" / >
< / div >
) ;
} ;
2019-05-06 21:35:29 +01:00
2023-01-19 17:19:06 +00:00
const mapStateToProps = ( state : AppState ) = > {
2019-05-06 21:35:29 +01:00
return {
2020-09-15 14:01:07 +01:00
themeId : state.settings.theme ,
2025-01-13 08:33:42 -08:00
scrollbarSize : state.settings [ 'style.scrollbarSize' ] ,
2019-05-06 21:35:29 +01:00
} ;
} ;
const NoteRevisionViewer = connect ( mapStateToProps ) ( NoteRevisionViewerComponent ) ;
2023-01-19 17:19:06 +00:00
export default NoteRevisionViewer ;