diff --git a/apps/client/src/features/page-history/components/history-diff.module.css b/apps/client/src/features/page-history/components/history-diff.module.css index 3219853f..6b5af8bc 100644 --- a/apps/client/src/features/page-history/components/history-diff.module.css +++ b/apps/client/src/features/page-history/components/history-diff.module.css @@ -32,3 +32,19 @@ border-radius: rem(2px); padding: 0 rem(2px); } + +:global(.history-diff-node-added) { + outline: rem(2px) solid light-dark(var(--mantine-color-green-5), var(--mantine-color-green-7)); + outline-offset: rem(2px); + border-radius: rem(4px); +} + +:global(.history-diff-node-deleted) { + display: inline-block; + background: light-dark(var(--mantine-color-red-0), rgba(255, 0, 0, 0.1)); + border: rem(1px) dashed light-dark(var(--mantine-color-red-4), var(--mantine-color-red-6)); + border-radius: rem(4px); + padding: rem(4px) rem(8px); + color: light-dark(var(--mantine-color-red-7), var(--mantine-color-red-4)); + font-size: rem(12px); +} diff --git a/apps/client/src/features/page-history/components/history-editor.tsx b/apps/client/src/features/page-history/components/history-editor.tsx index dc21a82b..20741662 100644 --- a/apps/client/src/features/page-history/components/history-editor.tsx +++ b/apps/client/src/features/page-history/components/history-editor.tsx @@ -64,27 +64,82 @@ export function HistoryEditor({ editor.commands.setContent(content); + const specialNodeTypes = new Set([ + "image", + "attachment", + "video", + "excalidraw", + "drawio", + "mermaid", + "mathBlock", + "mathInline", + "table", + "details", + "callout", + ]); + const decorations: Decoration[] = []; for (const change of changes) { if (change.toB > change.fromB) { - decorations.push( - Decoration.inline(change.fromB, change.toB, { - class: "history-diff-added", - }), - ); + let foundSpecialNode: { node: Node; pos: number } | null = null; + docNew.nodesBetween(change.fromB, change.toB, (node, pos) => { + if (specialNodeTypes.has(node.type.name)) { + foundSpecialNode = { node, pos }; + return false; + } + }); + + if (foundSpecialNode) { + const nodeEnd = + foundSpecialNode.pos + foundSpecialNode.node.nodeSize; + decorations.push( + Decoration.node(foundSpecialNode.pos, nodeEnd, { + class: "history-diff-node-added", + }), + ); + } else { + decorations.push( + Decoration.inline(change.fromB, change.toB, { + class: "history-diff-added", + }), + ); + } addedCount += 1; } if (change.toA > change.fromA) { - const deletedText = docOld.textBetween(change.fromA, change.toA, ""); - if (deletedText) { + let foundDeletedNode: { node: Node; pos: number } | null = null; + docOld.nodesBetween(change.fromA, change.toA, (node, pos) => { + if (specialNodeTypes.has(node.type.name)) { + foundDeletedNode = { node, pos }; + return false; + } + }); + + if (foundDeletedNode) { decorations.push( Decoration.widget(change.fromB, () => { const span = document.createElement("span"); - span.className = "history-diff-deleted"; - span.textContent = deletedText; + span.className = "history-diff-node-deleted"; + span.textContent = `[${foundDeletedNode!.node.type.name} removed]`; return span; }), ); + } else { + const deletedText = docOld.textBetween( + change.fromA, + change.toA, + "", + ); + if (deletedText) { + decorations.push( + Decoration.widget(change.fromB, () => { + const span = document.createElement("span"); + span.className = "history-diff-deleted"; + span.textContent = deletedText; + return span; + }), + ); + } } deletedCount += 1; } @@ -104,7 +159,8 @@ export function HistoryEditor({ editor.setOptions({ editorProps: { ...editor.options.editorProps, - decorations: () => (highlightChanges ? decorationSet : DecorationSet.empty), + decorations: () => + highlightChanges ? decorationSet : DecorationSet.empty, }, }); }, [title, content, editor, previousContent, highlightChanges]); diff --git a/apps/server/src/collaboration/listeners/history.listener.ts b/apps/server/src/collaboration/listeners/history.listener.ts index 958e288e..6096122b 100644 --- a/apps/server/src/collaboration/listeners/history.listener.ts +++ b/apps/server/src/collaboration/listeners/history.listener.ts @@ -3,6 +3,7 @@ import { OnEvent } from '@nestjs/event-emitter'; import { PageHistoryRepo } from '@docmost/db/repos/page/page-history.repo'; import { Page } from '@docmost/db/types/entity.types'; import { isDeepStrictEqual } from 'node:util'; +import { EnvironmentService } from '../../integrations/environment/environment.service'; export class UpdatedPageEvent { page: Page; @@ -12,7 +13,10 @@ export class UpdatedPageEvent { export class HistoryListener { private readonly logger = new Logger(HistoryListener.name); - constructor(private readonly pageHistoryRepo: PageHistoryRepo) {} + constructor( + private readonly pageHistoryRepo: PageHistoryRepo, + private readonly environmentService: EnvironmentService, + ) {} @OnEvent('collab.page.updated') async handleCreatePageHistory(event: UpdatedPageEvent) { @@ -20,7 +24,9 @@ export class HistoryListener { const pageCreationTime = new Date(page.createdAt).getTime(); const currentTime = Date.now(); - const FIVE_MINUTES = 5 * 60 * 1000; + const FIVE_MINUTES = this.environmentService.isDevelopment() + ? 30 * 1000 + : 5 * 60 * 1000; if (currentTime - pageCreationTime < FIVE_MINUTES) { return;