import type { RevisionPojo, RevisionItem } from "@triliumnext/commons"; import appContext from "../../components/app_context"; import FNote from "../../entities/fnote"; import dialog from "../../services/dialog"; import froca from "../../services/froca"; import { t } from "../../services/i18n"; import server from "../../services/server"; import toast from "../../services/toast"; import Button from "../react/Button"; import Modal from "../react/Modal"; import FormList, { FormListItem } from "../react/FormList"; import utils from "../../services/utils"; import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks"; import protected_session_holder from "../../services/protected_session_holder"; import { renderMathInElement } from "../../services/math"; import type { CSSProperties } from "preact/compat"; import open from "../../services/open"; import ActionButton from "../react/ActionButton"; import options from "../../services/options"; import { useTriliumEvent } from "../react/hooks"; import { diffWords } from "diff"; export default function RevisionsDialog() { const [ note, setNote ] = useState(); const [ noteContent, setNoteContent ] = useState(); const [ revisions, setRevisions ] = useState(); const [ currentRevision, setCurrentRevision ] = useState(); const [ shown, setShown ] = useState(false); const [ showDiff, setShowDiff ] = useState(false); const [ refreshCounter, setRefreshCounter ] = useState(0); useTriliumEvent("showRevisions", async ({ noteId }) => { const note = await getNote(noteId); if (note) { setNote(note); setShown(true); } }); useEffect(() => { if (note?.noteId) { server.get(`notes/${note.noteId}/revisions`).then(setRevisions); note.getContent().then(setNoteContent); } else { setRevisions(undefined); setNoteContent(undefined); } }, [ note?.noteId, refreshCounter ]); if (revisions?.length && !currentRevision) { setCurrentRevision(revisions[0]); } return ( { const text = t("revisions.confirm_delete_all"); if (note && await dialog.confirm(text)) { await server.remove(`notes/${note.noteId}/revisions`); setRevisions([]); setCurrentRevision(undefined); toast.showMessage(t("revisions.revisions_deleted")); } }}/>) } footer={} footerStyle={{ paddingTop: 0, paddingBottom: 0 }} onHidden={() => { setShown(false); setShowDiff(false); setNote(undefined); setCurrentRevision(undefined); setRevisions(undefined); }} show={shown} > { const correspondingRevision = (revisions ?? []).find((r) => r.revisionId === revisionId); if (correspondingRevision) { setCurrentRevision(correspondingRevision); } }} currentRevision={currentRevision} />
{ setRefreshCounter(c => c + 1); setCurrentRevision(undefined); }} />
) } function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: RevisionItem[], onSelect: (val: string) => void, currentRevision?: RevisionItem }) { return ( {revisions.map((item) => {item.dateLastEdited && item.dateLastEdited.substr(0, 16)} ({item.contentLength && utils.formatSize(item.contentLength)}) )} ); } function RevisionPreview({noteContent, revisionItem, setShown, showDiff, setShowDiff, onRevisionDeleted }: { noteContent?: string, revisionItem?: RevisionItem, setShown: Dispatch>, showDiff: boolean, setShowDiff: Dispatch>, onRevisionDeleted?: () => void }) { const [ fullRevision, setFullRevision ] = useState(); useEffect(() => { if (revisionItem) { server.get(`revisions/${revisionItem.revisionId}`).then(setFullRevision); } else { setFullRevision(undefined); } }, [revisionItem]); return ( <>

{revisionItem?.title ?? t("revisions.no_revisions")}

{(revisionItem &&
{(!revisionItem.isProtected || protected_session_holder.isProtectedSessionAvailable()) && <> {["text", "code", "mermaid"].includes(revisionItem.type) && (
)}
); } const IMAGE_STYLE: CSSProperties = { maxWidth: "100%", maxHeight: "90%", objectFit: "contain" }; const CODE_STYLE: CSSProperties = { maxWidth: "100%", wordBreak: "break-all", whiteSpace: "pre-wrap" }; function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }: { noteContent?:string, revisionItem?: RevisionItem, fullRevision?: RevisionPojo, showDiff: boolean}) { const content = fullRevision?.content; if (!revisionItem || !content) { return <>; } if (showDiff) { return } switch (revisionItem.type) { case "text": return case "code": return
{content}
; case "image": switch (revisionItem.mime) { case "image/svg+xml": { //Base64 of other format images may be embedded in svg const encodedSVG = encodeURIComponent(content as string); return ; } default: { // the reason why we put this inline as base64 is that we do not want to let user copy this // as a URL to be used in a note. Instead, if they copy and paste it into a note, it will be uploaded as a new note return } } case "file": return {fullRevision.content && }
{t("revisions.mime")} {revisionItem.mime}
{t("revisions.file_size")} {revisionItem.contentLength && utils.formatSize(revisionItem.contentLength)}
{t("revisions.preview")}
{fullRevision.content}
; case "canvas": case "mindMap": case "mermaid": { const encodedTitle = encodeURIComponent(revisionItem.title); return ; } default: return <>{t("revisions.preview_not_available")} } } function RevisionContentText({ content }: { content: string | Buffer | undefined }) { const contentRef = useRef(null); useEffect(() => { if (contentRef.current?.querySelector("span.math-tex")) { renderMathInElement(contentRef.current, { trust: true }); } }, [content]); return
} function RevisionContentDiff({ noteContent, itemContent, itemType }: { noteContent?: string, itemContent: string | Buffer | undefined, itemType: string }) { let diffHtml: string; if (noteContent && typeof itemContent === "string") { if (itemType === "text") { noteContent = utils.formatHtml(noteContent); itemContent = utils.formatHtml(itemContent); } const diff = diffWords(noteContent, itemContent); diffHtml = diff.map(part => { if (part.added) { return `${utils.escapeHtml(part.value)}`; } else if (part.removed) { return `${utils.escapeHtml(part.value)}`; } else { return utils.escapeHtml(part.value); } }).join(""); } else { return <>{t("revisions.diff_not_available")} } return (
); } function RevisionFooter({ note }: { note?: FNote }) { if (!note) { return <>; } let revisionsNumberLimit: number | string = parseInt(note?.getLabelValue("versioningLimit") ?? ""); if (!Number.isInteger(revisionsNumberLimit)) { revisionsNumberLimit = options.getInt("revisionSnapshotNumberLimit") ?? 0; } if (revisionsNumberLimit === -1) { revisionsNumberLimit = "∞"; } return <> {t("revisions.snapshot_interval", { seconds: options.getInt("revisionSnapshotTimeInterval") })} {t("revisions.maximum_revisions", { number: revisionsNumberLimit })} appContext.tabManager.openContextWithNote("_optionsOther", { activate: true })} /> ; } async function getNote(noteId?: string | null) { if (noteId) { return await froca.getNote(noteId); } else { return appContext.tabManager.getActiveContextNote(); } }