2019-05-06 22:35:29 +02:00
const React = require ( 'react' ) ;
const { connect } = require ( 'react-redux' ) ;
2020-11-07 17:59:37 +02:00
const { themeStyle } = require ( '@joplin/lib/theme' ) ;
const { _ } = require ( '@joplin/lib/locale' ) ;
2020-09-29 09:40:14 +02:00
const NoteTextViewer = require ( './NoteTextViewer' ) . default ;
2019-05-06 22:35:29 +02:00
const HelpButton = require ( './HelpButton.min' ) ;
2020-11-07 17:59:37 +02:00
const BaseModel = require ( '@joplin/lib/BaseModel' ) . default ;
const Revision = require ( '@joplin/lib/models/Revision' ) ;
const urlUtils = require ( '@joplin/lib/urlUtils' ) ;
const Setting = require ( '@joplin/lib/models/Setting' ) . default ;
const RevisionService = require ( '@joplin/lib/services/RevisionService' ) ;
const shared = require ( '@joplin/lib/components/shared/note-screen-shared.js' ) ;
const { MarkupToHtml } = require ( '@joplin/renderer' ) ;
const time = require ( '@joplin/lib/time' ) . default ;
2019-05-06 22:35:29 +02:00
const ReactTooltip = require ( 'react-tooltip' ) ;
2020-11-07 17:59:37 +02:00
const { urlDecode , substrWithEllipsis } = require ( '@joplin/lib/string-utils' ) ;
2020-10-09 19:35:46 +02:00
const bridge = require ( 'electron' ) . remote . require ( './bridge' ) . default ;
2020-11-07 17:59:37 +02:00
const markupLanguageUtils = require ( '@joplin/lib/markupLanguageUtils' ) . default ;
2019-05-06 22:35:29 +02:00
class NoteRevisionViewerComponent extends React . PureComponent {
constructor ( ) {
super ( ) ;
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
}
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' ,
} ,
titleInput : Object . assign ( { } , theme . inputStyle , { flex : 1 } ) ,
revisionList : Object . assign ( { } , theme . dropdownList , { marginLeft : 10 , flex : 0.5 } ) ,
} ;
return style ;
}
async viewer _domReady ( ) {
// this.viewerRef_.current.wrappedInstance.openDevTools();
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 : '' ,
} ,
( ) => {
this . reloadNote ( ) ;
}
) ;
2019-05-06 22:35:29 +02:00
}
async importButton _onClick ( ) {
if ( ! this . state . note ) return ;
this . setState ( { restoring : true } ) ;
await RevisionService . instance ( ) . importRevisionNote ( this . state . note ) ;
this . setState ( { restoring : false } ) ;
alert ( _ ( 'The note "%s" has been successfully restored to the notebook "%s".' , substrWithEllipsis ( this . state . note . title , 0 , 32 ) , RevisionService . instance ( ) . restoreFolderTitle ( ) ) ) ;
}
backButton _click ( ) {
if ( this . props . onBack ) this . props . onBack ( ) ;
}
revisionList _onChange ( event ) {
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 ,
} ,
( ) => {
this . reloadNote ( ) ;
}
) ;
2019-05-06 22:35:29 +02:00
}
}
async reloadNote ( ) {
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' ) } / ` ,
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 ,
userCss : this . props . customCss ? this . props . customCss : '' ,
resources : await shared . attachedResources ( noteBody ) ,
2019-10-28 20:47:23 +02:00
postMessageSyntax : 'ipcProxySendToHost' ,
2019-05-06 22:35:29 +02:00
} ) ;
2020-01-22 19:32:21 +02:00
this . viewerRef _ . current . wrappedInstance . send ( 'setHtml' , result . html , {
cssFiles : result . cssFiles ,
pluginAssets : result . pluginAssets ,
} ) ;
2019-05-06 22:35:29 +02:00
}
2019-10-28 20:47:23 +02:00
async webview _ipcMessage ( event ) {
// 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 ) {
require ( 'electron' ) . shell . openExternal ( urlDecode ( msg ) ) ;
} else {
require ( 'electron' ) . shell . openExternal ( msg ) ;
}
} 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 ) ;
}
}
2019-05-06 22:35:29 +02:00
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 } ) ` }
2019-07-29 14:13:23 +02:00
< / option >
) ;
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 } } >
< button onClick = { this . backButton _click } style = { Object . assign ( { } , 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 >
2019-07-29 14:13:23 +02:00
< button disabled = { ! this . state . revisions . length || this . state . restoring } onClick = { this . importButton _onClick } style = { Object . assign ( { } , theme . buttonStyle , { marginLeft : 10 , height : theme . inputStyle . height } ) } >
{ restoreButtonTitle }
< / button >
< HelpButton tip = { helpMessage } id = "noteRevisionHelpButton" onClick = { this . helpButton _onClick } / >
2019-05-06 22:35:29 +02:00
< / div >
) ;
2020-01-22 19:32:21 +02:00
const viewer = < NoteTextViewer 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 (
< div style = { style . root } >
{ 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 >
) ;
}
}
2020-05-21 10:14:33 +02:00
const mapStateToProps = state => {
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 ) ;
2019-07-29 14:13:23 +02:00
module . exports = NoteRevisionViewer ;