Use tracked changes and comments from the snapshot (#26267)

GitOrigin-RevId: c2bf0c9c9a5ab4f8837b8712ca549119a31cf067
This commit is contained in:
Alf Eaton
2025-09-05 08:05:56 +00:00
committed by Copybot
parent 19866f48a8
commit 2c1baa717d
21 changed files with 737 additions and 267 deletions
@@ -29,6 +29,10 @@ import {
import { ThreadId } from '../../../../../types/review-panel/review-panel'
import getMeta from '@/utils/meta'
import OError from '@overleaf/o-error'
import {
HistoryOTShareDoc,
ShareLatexOTShareDoc,
} from '../../../../../types/share-doc'
const MAX_PENDING_OP_SIZE = 64
@@ -121,6 +125,27 @@ export class DocumentContainer extends EventEmitter {
this.bindToSocketEvents()
}
get shareDoc() {
if (!this.doc) {
throw new Error('Missing ShareJSDoc')
}
if (!this.doc._doc) {
throw new Error('Missing ShareJS Doc')
}
return this.doc._doc as HistoryOTShareDoc | ShareLatexOTShareDoc
}
isHistoryOT() {
return this.shareDoc.otType === 'history-ot'
}
get historyOTShareDoc() {
if (!this.isHistoryOT()) {
throw new Error('shareDoc is not historyOT')
}
return this.shareDoc as HistoryOTShareDoc
}
attachToCM6(cm6: EditorFacade) {
this.cm6 = cm6
if (this.doc) {
@@ -613,6 +638,9 @@ export class DocumentContainer extends EventEmitter {
this.trigger('op:timeout')
return this.onError(new Error('op timed out'))
})
this.doc.on('ranges:dirty', (...args) => {
return this.trigger('ranges:dirty', ...args)
})
let docChangedTimeout: number | null = null
this.doc.on(
@@ -22,11 +22,13 @@ import {
StringFileData,
TrackedChangeList,
EditOperationBuilder,
CommentList,
} from 'overleaf-editor-core'
import {
StringFileRawData,
RawEditOperation,
} from 'overleaf-editor-core/lib/types'
import { HistoryOTShareDoc } from '../../../../../types/share-doc'
// All times below are in milliseconds
const SINGLE_USER_FLUSH_DELAY = 2000
@@ -267,11 +269,26 @@ export class ShareJsDoc extends EventEmitter {
processUpdateFromServer(message: Message) {
try {
if (this.type === 'history-ot' && message.op != null) {
const shareDoc = this._doc as HistoryOTShareDoc
const trackedChangesBefore = shareDoc.snapshot.getTrackedChanges()
const commentsBefore = shareDoc.snapshot.getComments()
const ops = message.op as RawEditOperation[]
this._doc._onMessage({
...message,
op: ops.map(EditOperationBuilder.fromJSON),
})
if (
this.rangesUpdated(
trackedChangesBefore,
commentsBefore,
shareDoc.snapshot.getTrackedChanges(),
shareDoc.snapshot.getComments()
)
) {
this.trigger('ranges:dirty')
}
} else {
this._doc._onMessage(message)
}
@@ -471,6 +488,29 @@ export class ShareJsDoc extends EventEmitter {
doc.pendingCallbacks.push(() => {
return this.trigger('op:acknowledged', op)
})
// history-ot: submit the op and detect whether tracked changes or comments have updated
if (this.type === 'history-ot') {
const shareDoc = doc as HistoryOTShareDoc
const trackedChangesBefore = shareDoc.snapshot.getTrackedChanges()
const commentsBefore = shareDoc.snapshot.getComments()
const result = submitOp.call(doc, op, callback)
if (
this.rangesUpdated(
trackedChangesBefore,
commentsBefore,
shareDoc.snapshot.getTrackedChanges(),
shareDoc.snapshot.getComments()
)
) {
this.trigger('ranges:dirty')
}
return result
}
// non-history-ot: just submit the op
return submitOp.call(doc, op, callback)
}
@@ -480,4 +520,31 @@ export class ShareJsDoc extends EventEmitter {
return flush.call(doc)
}
}
private rangesUpdated(
trackedChangesBefore: TrackedChangeList,
commentsBefore: CommentList,
trackedChangesAfter: TrackedChangeList,
commentsAfter: CommentList
) {
return (
// quick length comparison first
trackedChangesBefore.length !== trackedChangesAfter.length ||
commentsBefore.length !== commentsAfter.length ||
// then compare each item by identity
this.itemsChanged(
trackedChangesBefore.asSorted(),
trackedChangesAfter.asSorted()
) ||
this.itemsChanged(commentsBefore.toArray(), commentsAfter.toArray())
)
}
private itemsChanged(before: readonly any[], after: readonly any[]) {
for (let i = 0; i < before.length; i++) {
if (before[i] !== after[i]) {
return true
}
}
}
}