2023-01-19 19:19:06 +02:00
import * as React from 'react' ;
import { themeStyle } from '@joplin/lib/theme' ;
import { _ } from '@joplin/lib/locale' ;
import NoteTextViewer from './NoteTextViewer' ;
import HelpButton from './HelpButton' ;
import BaseModel from '@joplin/lib/BaseModel' ;
import Revision from '@joplin/lib/models/Revision' ;
import Setting from '@joplin/lib/models/Setting' ;
import RevisionService from '@joplin/lib/services/RevisionService' ;
import { MarkupToHtml } from '@joplin/renderer' ;
import time from '@joplin/lib/time' ;
import bridge from '../services/bridge' ;
import markupLanguageUtils from '../utils/markupLanguageUtils' ;
import { NoteEntity , RevisionEntity } from '@joplin/lib/services/database/types' ;
import { AppState } from '../app.reducer' ;
2020-11-07 17:59:37 +02:00
const urlUtils = require ( '@joplin/lib/urlUtils' ) ;
2019-05-06 22:35:29 +02:00
const ReactTooltip = require ( 'react-tooltip' ) ;
2021-06-10 11:49:20 +02:00
const { urlDecode } = require ( '@joplin/lib/string-utils' ) ;
2023-01-19 19:19:06 +02:00
const { connect } = require ( 'react-redux' ) ;
2023-02-18 17:31:59 +02:00
import shared from '@joplin/lib/components/shared/note-screen-shared' ;
2023-01-19 19:19:06 +02:00
interface Props {
themeId : number ;
noteId : string ;
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2023-01-19 19:19:06 +02:00
onBack : Function ;
customCss : string ;
}
interface State {
note : NoteEntity ;
revisions : RevisionEntity [ ] ;
currentRevId : string ;
restoring : boolean ;
}
class NoteRevisionViewerComponent extends React . PureComponent < Props , State > {
private viewerRef_ : any ;
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2023-01-19 19:19:06 +02:00
private helpButton_onClick : Function ;
2019-05-06 22:35:29 +02:00
2023-03-06 16:22:01 +02:00
public constructor ( props : Props ) {
2023-01-19 19:19:06 +02:00
super ( props ) ;
2019-05-06 22:35:29 +02:00
this . state = {
revisions : [ ] ,
currentRevId : '' ,
note : null ,
restoring : false ,
} ;
this . viewerRef_ = React . createRef ( ) ;
this . viewer_domReady = this . viewer_domReady . bind ( this ) ;
this . revisionList_onChange = this . revisionList_onChange . bind ( this ) ;
this . importButton_onClick = this . importButton_onClick . bind ( this ) ;
this . backButton_click = this . backButton_click . bind ( this ) ;
2019-10-28 20:47:23 +02:00
this . webview_ipcMessage = this . webview_ipcMessage . bind ( this ) ;
2019-05-06 22:35:29 +02:00
}
2023-03-06 16:22:01 +02:00
public style() {
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( this . props . themeId ) ;
2019-05-06 22:35:29 +02:00
2020-03-14 01:46:14 +02:00
const style = {
2019-05-06 22:35:29 +02:00
root : {
backgroundColor : theme.backgroundColor ,
display : 'flex' ,
flex : 1 ,
flexDirection : 'column' ,
} ,
2023-06-01 13:02:36 +02:00
titleInput : { . . . theme . inputStyle , flex : 1 } ,
revisionList : { . . . theme . dropdownList , marginLeft : 10 , flex : 0.5 } ,
2019-05-06 22:35:29 +02:00
} ;
return style ;
}
2023-03-06 16:22:01 +02:00
private async viewer_domReady() {
2022-11-14 18:48:41 +02:00
// this.viewerRef_.current.openDevTools();
2019-05-06 22:35:29 +02:00
const revisions = await Revision . allByType ( BaseModel . TYPE_NOTE , this . props . noteId ) ;
2019-07-29 14:13:23 +02:00
this . setState (
{
revisions : revisions ,
currentRevId : revisions.length ? revisions [ revisions . length - 1 ] . id : '' ,
} ,
( ) = > {
2023-01-19 19:19:06 +02:00
void this . reloadNote ( ) ;
2023-08-22 12:58:53 +02:00
} ,
2019-07-29 14:13:23 +02:00
) ;
2019-05-06 22:35:29 +02:00
}
2023-03-06 16:22:01 +02:00
private async importButton_onClick() {
2019-05-06 22:35:29 +02:00
if ( ! this . state . note ) return ;
this . setState ( { restoring : true } ) ;
await RevisionService . instance ( ) . importRevisionNote ( this . state . note ) ;
this . setState ( { restoring : false } ) ;
2021-06-10 11:49:20 +02:00
alert ( RevisionService . instance ( ) . restoreSuccessMessage ( this . state . note ) ) ;
2019-05-06 22:35:29 +02:00
}
2023-03-06 16:22:01 +02:00
private backButton_click() {
2019-05-06 22:35:29 +02:00
if ( this . props . onBack ) this . props . onBack ( ) ;
}
2023-03-06 16:22:01 +02:00
private revisionList_onChange ( event : any ) {
2019-05-06 22:35:29 +02:00
const value = event . target . value ;
if ( ! value ) {
if ( this . props . onBack ) this . props . onBack ( ) ;
} else {
2019-07-29 14:13:23 +02:00
this . setState (
{
currentRevId : value ,
} ,
( ) = > {
2023-01-19 19:19:06 +02:00
void this . reloadNote ( ) ;
2023-08-22 12:58:53 +02:00
} ,
2019-07-29 14:13:23 +02:00
) ;
2019-05-06 22:35:29 +02:00
}
}
2023-03-06 16:22:01 +02:00
public async reloadNote() {
2019-05-06 22:35:29 +02:00
let noteBody = '' ;
2019-12-29 19:58:40 +02:00
let markupLanguage = MarkupToHtml . MARKUP_LANGUAGE_MARKDOWN ;
2019-05-06 22:35:29 +02:00
if ( ! this . state . revisions . length || ! this . state . currentRevId ) {
noteBody = _ ( 'This note has no history' ) ;
this . setState ( { note : null } ) ;
} else {
const revIndex = BaseModel . modelIndexById ( this . state . revisions , this . state . currentRevId ) ;
const note = await RevisionService . instance ( ) . revisionNote ( this . state . revisions , revIndex ) ;
if ( ! note ) return ;
noteBody = note . body ;
2019-07-19 18:48:38 +02:00
markupLanguage = note . markup_language ;
2019-05-06 22:35:29 +02:00
this . setState ( { note : note } ) ;
}
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( this . props . themeId ) ;
2019-05-06 22:35:29 +02:00
2020-12-19 19:42:18 +02:00
const markupToHtml = markupLanguageUtils . newMarkupToHtml ( { } , {
2019-09-19 23:51:18 +02:00
resourceBaseUrl : ` file:// ${ Setting . value ( 'resourceDir' ) } / ` ,
2021-05-19 15:00:16 +02:00
customCss : this.props.customCss ? this . props . customCss : '' ,
2019-05-06 22:35:29 +02:00
} ) ;
2019-12-29 19:58:40 +02:00
const result = await markupToHtml . render ( markupLanguage , noteBody , theme , {
2019-05-06 22:35:29 +02:00
codeTheme : theme.codeThemeCss ,
resources : await shared . attachedResources ( noteBody ) ,
2019-10-28 20:47:23 +02:00
postMessageSyntax : 'ipcProxySendToHost' ,
2019-05-06 22:35:29 +02:00
} ) ;
2022-11-14 18:48:41 +02:00
this . viewerRef_ . current . send ( 'setHtml' , result . html , {
2023-01-19 19:19:06 +02:00
// cssFiles: result.cssFiles,
2020-01-22 19:32:21 +02:00
pluginAssets : result.pluginAssets ,
} ) ;
2019-05-06 22:35:29 +02:00
}
2023-03-06 16:22:01 +02:00
private async webview_ipcMessage ( event : any ) {
2019-10-28 20:47:23 +02:00
// For the revision view, we only suppport a minimal subset of the IPC messages.
// 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 21:56:53 +02:00
// const args = event.args;
2019-10-28 20:47:23 +02:00
2020-07-23 21:56:53 +02:00
// if (msg !== 'percentScroll') console.info(`Got ipc-message: ${msg}`, args);
2019-10-28 20:47:23 +02:00
try {
if ( msg . indexOf ( 'joplin://' ) === 0 ) {
throw new Error ( _ ( 'Unsupported link or message: %s' , msg ) ) ;
} else if ( urlUtils . urlProtocol ( msg ) ) {
if ( msg . indexOf ( 'file://' ) === 0 ) {
2023-01-19 19:19:06 +02:00
void require ( 'electron' ) . shell . openExternal ( urlDecode ( msg ) ) ;
2019-10-28 20:47:23 +02:00
} else {
2023-01-19 19:19:06 +02:00
void require ( 'electron' ) . shell . openExternal ( msg ) ;
2019-10-28 20:47:23 +02: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 ) ;
}
}
2023-03-06 16:22:01 +02:00
public render() {
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( this . props . themeId ) ;
2019-05-06 22:35:29 +02:00
const style = this . style ( ) ;
const revisionListItems = [ ] ;
const revs = this . state . revisions . slice ( ) . reverse ( ) ;
for ( let i = 0 ; i < revs . length ; i ++ ) {
const rev = revs [ i ] ;
2019-05-24 18:31:18 +02:00
const stats = Revision . revisionPatchStatsText ( rev ) ;
2019-07-29 14:13:23 +02:00
revisionListItems . push (
< option key = { rev . id } value = { rev . id } >
2019-09-19 23:51:18 +02:00
{ ` ${ time . formatMsToLocal ( rev . item_updated_time ) } ( ${ stats } ) ` }
2023-08-22 12:58:53 +02:00
< / option > ,
2019-07-29 14:13:23 +02:00
) ;
2019-05-06 22:35:29 +02: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 = (
2019-07-29 14:13:23 +02:00
< div style = { { display : 'flex' , flexDirection : 'row' , alignItems : 'center' , marginBottom : 10 , borderWidth : 1 , borderBottomStyle : 'solid' , borderColor : theme.dividerColor , paddingBottom : 10 } } >
2023-06-01 13:02:36 +02:00
< button onClick = { this . backButton_click } style = { { . . . theme . buttonStyle , marginRight : 10 , height : theme.inputStyle.height } } >
2019-10-28 20:47:23 +02:00
< i style = { theme . buttonIconStyle } className = { 'fa fa-chevron-left' } > < / i > { _ ( 'Back' ) }
2019-07-29 14:13:23 +02:00
< / button >
< input readOnly type = "text" style = { style . titleInput } value = { this . state . note ? this . state . note . title : '' } / >
2019-05-06 22:35:29 +02:00
< select disabled = { ! this . state . revisions . length } value = { this . state . currentRevId } style = { style . revisionList } onChange = { this . revisionList_onChange } >
{ revisionListItems }
< / select >
2023-06-01 13:02:36 +02:00
< button disabled = { ! this . state . revisions . length || this . state . restoring } onClick = { this . importButton_onClick } style = { { . . . theme . buttonStyle , marginLeft : 10 , height : theme.inputStyle.height } } >
2019-07-29 14:13:23 +02:00
{ restoreButtonTitle }
< / button >
< HelpButton tip = { helpMessage } id = "noteRevisionHelpButton" onClick = { this . helpButton_onClick } / >
2019-05-06 22:35:29 +02:00
< / div >
) ;
2022-11-14 18:48:41 +02:00
const viewer = < NoteTextViewer themeId = { this . props . themeId } viewerStyle = { { display : 'flex' , flex : 1 , borderLeft : 'none' } } ref = { this . viewerRef_ } onDomReady = { this . viewer_domReady } onIpcMessage = { this . webview_ipcMessage } / > ;
2019-05-06 22:35:29 +02:00
return (
2023-01-19 19:19:06 +02:00
< div style = { style . root as any } >
2019-05-06 22:35:29 +02:00
{ titleInput }
{ viewer }
2019-07-29 14:13:23 +02:00
< ReactTooltip place = "bottom" delayShow = { 300 } className = "help-tooltip" / >
2019-05-06 22:35:29 +02:00
< / div >
) ;
}
}
2023-01-19 19:19:06 +02:00
const mapStateToProps = ( state : AppState ) = > {
2019-05-06 22:35:29 +02:00
return {
2020-09-15 15:01:07 +02:00
themeId : state.settings.theme ,
2019-05-06 22:35:29 +02:00
} ;
} ;
const NoteRevisionViewer = connect ( mapStateToProps ) ( NoteRevisionViewerComponent ) ;
2023-01-19 19:19:06 +02:00
export default NoteRevisionViewer ;