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'; const urlUtils = require('@joplin/lib/urlUtils'); const ReactTooltip = require('react-tooltip'); const { connect } = require('react-redux'); import shared from '@joplin/lib/components/shared/note-screen-shared'; interface Props { themeId: number; noteId: string; // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied onBack: Function; customCss: string; } interface State { note: NoteEntity; revisions: RevisionEntity[]; currentRevId: string; restoring: boolean; } class NoteRevisionViewerComponent extends React.PureComponent { private viewerRef_: any; // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied private helpButton_onClick: Function; public constructor(props: Props) { super(props); 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); } public style() { const theme = themeStyle(this.props.themeId); const style = { root: { backgroundColor: theme.backgroundColor, display: 'flex', flex: 1, flexDirection: 'column', }, titleInput: { ...theme.inputStyle, flex: 1 }, revisionList: { ...theme.dropdownList, marginLeft: 10, flex: 0.5 }, }; return style; } private async viewer_domReady() { // this.viewerRef_.current.openDevTools(); const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, this.props.noteId); this.setState( { revisions: revisions, currentRevId: revisions.length ? revisions[revisions.length - 1].id : '', }, () => { void this.reloadNote(); }, ); } private async importButton_onClick() { if (!this.state.note) return; this.setState({ restoring: true }); await RevisionService.instance().importRevisionNote(this.state.note); this.setState({ restoring: false }); alert(RevisionService.instance().restoreSuccessMessage(this.state.note)); } private backButton_click() { if (this.props.onBack) this.props.onBack(); } private revisionList_onChange(event: any) { const value = event.target.value; if (!value) { if (this.props.onBack) this.props.onBack(); } else { this.setState( { currentRevId: value, }, () => { void this.reloadNote(); }, ); } } public 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')}/`, customCss: this.props.customCss ? this.props.customCss : '', }); const result = await markupToHtml.render(markupLanguage, noteBody, theme, { codeTheme: theme.codeThemeCss, resources: await shared.attachedResources(noteBody), postMessageSyntax: 'ipcProxySendToHost', }); this.viewerRef_.current.send('setHtml', result.html, { // cssFiles: result.cssFiles, pluginAssets: result.pluginAssets, }); } private async webview_ipcMessage(event: any) { // For the revision view, we only support 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)) { await bridge().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); } } public 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 = (
); const viewer = ; return (
{titleInput} {viewer}
); } } const mapStateToProps = (state: AppState) => { return { themeId: state.settings.theme, }; }; const NoteRevisionViewer = connect(mapStateToProps)(NoteRevisionViewerComponent); export default NoteRevisionViewer;