const React = require('react'); const { connect } = require('react-redux'); const { themeStyle } = require('@joplin/lib/theme'); const { _ } = require('@joplin/lib/locale'); const NoteTextViewer = require('./NoteTextViewer').default; const HelpButton = require('./HelpButton.min'); 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; const ReactTooltip = require('react-tooltip'); const { urlDecode, substrWithEllipsis } = require('@joplin/lib/string-utils'); const bridge = require('electron').remote.require('./bridge').default; const markupLanguageUtils = require('@joplin/lib/markupLanguageUtils').default; 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); this.webview_ipcMessage = this.webview_ipcMessage.bind(this); } style() { const theme = themeStyle(this.props.themeId); const style = { 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); this.setState( { revisions: revisions, currentRevId: revisions.length ? revisions[revisions.length - 1].id : '', }, () => { this.reloadNote(); } ); } 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 { this.setState( { currentRevId: value, }, () => { this.reloadNote(); } ); } } async reloadNote() { let noteBody = ''; let markupLanguage = MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN; 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; markupLanguage = note.markup_language; this.setState({ note: note }); } const theme = themeStyle(this.props.themeId); const markupToHtml = markupLanguageUtils.newMarkupToHtml({}, { resourceBaseUrl: `file://${Setting.value('resourceDir')}/`, }); const result = await markupToHtml.render(markupLanguage, noteBody, theme, { codeTheme: theme.codeThemeCss, userCss: this.props.customCss ? this.props.customCss : '', resources: await shared.attachedResources(noteBody), postMessageSyntax: 'ipcProxySendToHost', }); this.viewerRef_.current.wrappedInstance.send('setHtml', result.html, { cssFiles: result.cssFiles, pluginAssets: result.pluginAssets, }); } 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 : ''; // const args = event.args; // if (msg !== 'percentScroll') console.info(`Got ipc-message: ${msg}`, args); 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); } } render() { const theme = themeStyle(this.props.themeId); 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]; const stats = Revision.revisionPatchStatsText(rev); revisionListItems.push( ); } 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 = (