|
@@ -1,6 +1,9 @@
|
|
|
import { useState, useEffect, useMemo, useCallback, useRef } from 'react'
|
|
import { useState, useEffect, useMemo, useCallback, useRef } from 'react'
|
|
|
|
|
+import { isEqual, cloneDeep } from 'lodash'
|
|
|
import useScopeValue from '../../../../../shared/hooks/use-scope-value'
|
|
import useScopeValue from '../../../../../shared/hooks/use-scope-value'
|
|
|
import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
|
|
import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
|
|
|
|
|
+import useAsync from '@/shared/hooks/use-async'
|
|
|
|
|
+import useAbortController from '@/shared/hooks/use-abort-controller'
|
|
|
import { sendMB } from '../../../../../infrastructure/event-tracking'
|
|
import { sendMB } from '../../../../../infrastructure/event-tracking'
|
|
|
import { dispatchReviewPanelLayout as handleLayoutChange } from '@/features/source-editor/extensions/changes/change-manager'
|
|
import { dispatchReviewPanelLayout as handleLayoutChange } from '@/features/source-editor/extensions/changes/change-manager'
|
|
|
import { useProjectContext } from '@/shared/context/project-context'
|
|
import { useProjectContext } from '@/shared/context/project-context'
|
|
@@ -9,19 +12,49 @@ import { useUserContext } from '@/shared/context/user-context'
|
|
|
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
|
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
|
|
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
|
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
|
|
import { debugConsole } from '@/utils/debugging'
|
|
import { debugConsole } from '@/utils/debugging'
|
|
|
-import { postJSON } from '@/infrastructure/fetch-json'
|
|
|
|
|
-import { ReviewPanelStateReactIde } from '../types/review-panel-state'
|
|
|
|
|
|
|
+import { useEditorContext } from '@/shared/context/editor-context'
|
|
|
|
|
+import { getJSON, postJSON } from '@/infrastructure/fetch-json'
|
|
|
import ColorManager from '@/ide/colors/ColorManager'
|
|
import ColorManager from '@/ide/colors/ColorManager'
|
|
|
|
|
+// @ts-ignore
|
|
|
|
|
+import RangesTracker from '@overleaf/ranges-tracker'
|
|
|
|
|
+import { ReviewPanelStateReactIde } from '../types/review-panel-state'
|
|
|
import * as ReviewPanel from '../types/review-panel-state'
|
|
import * as ReviewPanel from '../types/review-panel-state'
|
|
|
import {
|
|
import {
|
|
|
|
|
+ ReviewPanelCommentThreadMessage,
|
|
|
|
|
+ ReviewPanelCommentThreads,
|
|
|
|
|
+ ReviewPanelDocEntries,
|
|
|
SubView,
|
|
SubView,
|
|
|
ThreadId,
|
|
ThreadId,
|
|
|
} from '../../../../../../../types/review-panel/review-panel'
|
|
} from '../../../../../../../types/review-panel/review-panel'
|
|
|
import { UserId } from '../../../../../../../types/user'
|
|
import { UserId } from '../../../../../../../types/user'
|
|
|
import { PublicAccessLevel } from '../../../../../../../types/public-access-level'
|
|
import { PublicAccessLevel } from '../../../../../../../types/public-access-level'
|
|
|
-import { DeepReadonly } from '../../../../../../../types/utils'
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ DeepReadonly,
|
|
|
|
|
+ MergeAndOverride,
|
|
|
|
|
+} from '../../../../../../../types/utils'
|
|
|
|
|
+import { ReviewPanelCommentThread } from '../../../../../../../types/review-panel/comment-thread'
|
|
|
|
|
+import { DocId } from '../../../../../../../types/project-settings'
|
|
|
|
|
+import {
|
|
|
|
|
+ ReviewPanelAggregateChangeEntry,
|
|
|
|
|
+ ReviewPanelChangeEntry,
|
|
|
|
|
+ ReviewPanelCommentEntry,
|
|
|
|
|
+ ReviewPanelEntry,
|
|
|
|
|
+} from '../../../../../../../types/review-panel/entry'
|
|
|
|
|
+import {
|
|
|
|
|
+ ReviewPanelCommentThreadMessageApi,
|
|
|
|
|
+ ReviewPanelCommentThreadsApi,
|
|
|
|
|
+} from '../../../../../../../types/review-panel/api'
|
|
|
|
|
+import { Document } from '@/features/ide-react/editor/document'
|
|
|
|
|
+
|
|
|
|
|
+const dispatchReviewPanelEvent = (type: string, payload?: any) => {
|
|
|
|
|
+ window.dispatchEvent(
|
|
|
|
|
+ new CustomEvent('review-panel:event', {
|
|
|
|
|
+ detail: { type, payload },
|
|
|
|
|
+ })
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-function formatUser(user: any): any {
|
|
|
|
|
|
|
+const formatUser = (user: any): any => {
|
|
|
let isSelf, name
|
|
let isSelf, name
|
|
|
const id =
|
|
const id =
|
|
|
(user != null ? user._id : undefined) ||
|
|
(user != null ? user._id : undefined) ||
|
|
@@ -62,6 +95,15 @@ function formatUser(user: any): any {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const formatComment = (
|
|
|
|
|
+ comment: ReviewPanelCommentThreadMessageApi
|
|
|
|
|
+): ReviewPanelCommentThreadMessage => {
|
|
|
|
|
+ const commentTyped = comment as unknown as ReviewPanelCommentThreadMessage
|
|
|
|
|
+ commentTyped.user = formatUser(comment.user)
|
|
|
|
|
+ commentTyped.timestamp = new Date(comment.timestamp)
|
|
|
|
|
+ return commentTyped
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function useReviewPanelState(): ReviewPanelStateReactIde {
|
|
function useReviewPanelState(): ReviewPanelStateReactIde {
|
|
|
const { reviewPanelOpen, setReviewPanelOpen } = useLayoutContext()
|
|
const { reviewPanelOpen, setReviewPanelOpen } = useLayoutContext()
|
|
|
const { projectId } = useIdeReactContext()
|
|
const { projectId } = useIdeReactContext()
|
|
@@ -71,10 +113,14 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|
|
const {
|
|
const {
|
|
|
features: { trackChangesVisible, trackChanges },
|
|
features: { trackChangesVisible, trackChanges },
|
|
|
} = project
|
|
} = project
|
|
|
|
|
+ const { isRestrictedTokenMember } = useEditorContext()
|
|
|
|
|
|
|
|
- const [subView, setSubView] = useScopeValue<ReviewPanel.Value<'subView'>>(
|
|
|
|
|
- 'reviewPanel.subView'
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ // TODO `currentDocument` and `currentDocumentId` should be get from `useEditorManagerContext()` but that makes tests fail
|
|
|
|
|
+ const [currentDocument] = useScopeValue<Document>('editor.sharejs_doc')
|
|
|
|
|
+ const [currentDocumentId] = useScopeValue<DocId>('editor.open_doc_id')
|
|
|
|
|
+
|
|
|
|
|
+ const [subView, setSubView] =
|
|
|
|
|
+ useState<ReviewPanel.Value<'subView'>>('cur_file')
|
|
|
const [loading] = useScopeValue<ReviewPanel.Value<'loading'>>(
|
|
const [loading] = useScopeValue<ReviewPanel.Value<'loading'>>(
|
|
|
'reviewPanel.overview.loading'
|
|
'reviewPanel.overview.loading'
|
|
|
)
|
|
)
|
|
@@ -84,29 +130,25 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|
|
const [collapsed, setCollapsed] = useScopeValue<
|
|
const [collapsed, setCollapsed] = useScopeValue<
|
|
|
ReviewPanel.Value<'collapsed'>
|
|
ReviewPanel.Value<'collapsed'>
|
|
|
>('reviewPanel.overview.docsCollapsedState')
|
|
>('reviewPanel.overview.docsCollapsedState')
|
|
|
- const [commentThreads] = useScopeValue<ReviewPanel.Value<'commentThreads'>>(
|
|
|
|
|
- 'reviewPanel.commentThreads',
|
|
|
|
|
- true
|
|
|
|
|
- )
|
|
|
|
|
- const [entries] = useScopeValue<ReviewPanel.Value<'entries'>>(
|
|
|
|
|
- 'reviewPanel.entries',
|
|
|
|
|
- true
|
|
|
|
|
- )
|
|
|
|
|
- const [loadingThreads] =
|
|
|
|
|
- useScopeValue<ReviewPanel.Value<'loadingThreads'>>('loadingThreads')
|
|
|
|
|
|
|
+ const [commentThreads, setCommentThreads] = useState<
|
|
|
|
|
+ ReviewPanel.Value<'commentThreads'>
|
|
|
|
|
+ >({})
|
|
|
|
|
+ const [entries, setEntries] = useState<ReviewPanel.Value<'entries'>>({})
|
|
|
|
|
|
|
|
const [permissions] =
|
|
const [permissions] =
|
|
|
useScopeValue<ReviewPanel.Value<'permissions'>>('permissions')
|
|
useScopeValue<ReviewPanel.Value<'permissions'>>('permissions')
|
|
|
- const [users] = useScopeValue<ReviewPanel.Value<'users'>>('users', true)
|
|
|
|
|
- const [resolvedComments] = useScopeValue<
|
|
|
|
|
|
|
+ const [users, setUsers] = useScopeValue<ReviewPanel.Value<'users'>>(
|
|
|
|
|
+ 'users',
|
|
|
|
|
+ true
|
|
|
|
|
+ )
|
|
|
|
|
+ const [resolvedComments, setResolvedComments] = useState<
|
|
|
ReviewPanel.Value<'resolvedComments'>
|
|
ReviewPanel.Value<'resolvedComments'>
|
|
|
- >('reviewPanel.resolvedComments', true)
|
|
|
|
|
|
|
+ >({})
|
|
|
|
|
|
|
|
const [wantTrackChanges, setWantTrackChanges] = useScopeValue<
|
|
const [wantTrackChanges, setWantTrackChanges] = useScopeValue<
|
|
|
ReviewPanel.Value<'wantTrackChanges'>
|
|
ReviewPanel.Value<'wantTrackChanges'>
|
|
|
>('editor.wantTrackChanges')
|
|
>('editor.wantTrackChanges')
|
|
|
- const [openDocId] =
|
|
|
|
|
- useScopeValue<ReviewPanel.Value<'openDocId'>>('editor.open_doc_id')
|
|
|
|
|
|
|
+ const openDocId = currentDocumentId
|
|
|
const [shouldCollapse, setShouldCollapse] =
|
|
const [shouldCollapse, setShouldCollapse] =
|
|
|
useState<ReviewPanel.Value<'shouldCollapse'>>(true)
|
|
useState<ReviewPanel.Value<'shouldCollapse'>>(true)
|
|
|
const [lineHeight] = useScopeValue<number>(
|
|
const [lineHeight] = useScopeValue<number>(
|
|
@@ -126,6 +168,325 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|
|
const [trackChangesForGuestsAvailable, setTrackChangesForGuestsAvailable] =
|
|
const [trackChangesForGuestsAvailable, setTrackChangesForGuestsAvailable] =
|
|
|
useState<ReviewPanel.Value<'trackChangesForGuestsAvailable'>>(false)
|
|
useState<ReviewPanel.Value<'trackChangesForGuestsAvailable'>>(false)
|
|
|
|
|
|
|
|
|
|
+ const [resolvedThreadIds, setResolvedThreadIds] = useState<
|
|
|
|
|
+ Record<ThreadId, boolean>
|
|
|
|
|
+ >({})
|
|
|
|
|
+
|
|
|
|
|
+ const {
|
|
|
|
|
+ isLoading: loadingThreads,
|
|
|
|
|
+ reset,
|
|
|
|
|
+ runAsync: runAsyncThreads,
|
|
|
|
|
+ } = useAsync<ReviewPanelCommentThreadsApi>()
|
|
|
|
|
+ const loadThreadsController = useAbortController()
|
|
|
|
|
+ const loadThreadsExecuted = useRef(false)
|
|
|
|
|
+ const ensureThreadsAreLoaded = useCallback(() => {
|
|
|
|
|
+ if (loadThreadsExecuted.current) {
|
|
|
|
|
+ // We get any updates in real time so only need to load them once.
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ loadThreadsExecuted.current = true
|
|
|
|
|
+
|
|
|
|
|
+ return runAsyncThreads(
|
|
|
|
|
+ getJSON(`/project/${projectId}/threads`, {
|
|
|
|
|
+ signal: loadThreadsController.signal,
|
|
|
|
|
+ })
|
|
|
|
|
+ )
|
|
|
|
|
+ .then(threads => {
|
|
|
|
|
+ const tempResolvedThreadIds: typeof resolvedThreadIds = {}
|
|
|
|
|
+ const threadsEntries = Object.entries(threads) as [
|
|
|
|
|
+ [
|
|
|
|
|
+ ThreadId,
|
|
|
|
|
+ MergeAndOverride<
|
|
|
|
|
+ ReviewPanelCommentThread,
|
|
|
|
|
+ ReviewPanelCommentThreadsApi[ThreadId]
|
|
|
|
|
+ >
|
|
|
|
|
+ ]
|
|
|
|
|
+ ]
|
|
|
|
|
+ for (const [threadId, thread] of threadsEntries) {
|
|
|
|
|
+ for (const comment of thread.messages) {
|
|
|
|
|
+ formatComment(comment)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (thread.resolved_by_user) {
|
|
|
|
|
+ thread.resolved_by_user = formatUser(thread.resolved_by_user)
|
|
|
|
|
+ tempResolvedThreadIds[threadId] = true
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ setResolvedThreadIds(tempResolvedThreadIds)
|
|
|
|
|
+ setCommentThreads(threads as unknown as ReviewPanelCommentThreads)
|
|
|
|
|
+
|
|
|
|
|
+ dispatchReviewPanelEvent('loaded_threads')
|
|
|
|
|
+ handleLayoutChange({ async: true })
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ resolvedThreadIds: tempResolvedThreadIds,
|
|
|
|
|
+ commentThreads: threads,
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(debugConsole.error)
|
|
|
|
|
+ }, [loadThreadsController.signal, projectId, runAsyncThreads])
|
|
|
|
|
+
|
|
|
|
|
+ const rangesTrackers = useRef<Record<DocId, RangesTracker>>({})
|
|
|
|
|
+ const refreshingRangeUsers = useRef(false)
|
|
|
|
|
+ const refreshedForUserIds = useRef(new Set<UserId>())
|
|
|
|
|
+ const refreshChangeUsers = useCallback(
|
|
|
|
|
+ (userId: UserId | null) => {
|
|
|
|
|
+ if (userId != null) {
|
|
|
|
|
+ if (refreshedForUserIds.current.has(userId)) {
|
|
|
|
|
+ // We've already tried to refresh to get this user id, so stop it looping
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ refreshedForUserIds.current.add(userId)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Only do one refresh at once
|
|
|
|
|
+ if (refreshingRangeUsers.current) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ refreshingRangeUsers.current = true
|
|
|
|
|
+
|
|
|
|
|
+ getJSON(`/project/${projectId}/changes/users`)
|
|
|
|
|
+ .then(usersResponse => {
|
|
|
|
|
+ refreshingRangeUsers.current = false
|
|
|
|
|
+ const tempUsers = {} as ReviewPanel.Value<'users'>
|
|
|
|
|
+ // Always include ourself, since if we submit an op, we might need to display info
|
|
|
|
|
+ // about it locally before it has been flushed through the server
|
|
|
|
|
+ if (user) {
|
|
|
|
|
+ tempUsers[user.id] = formatUser(user)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const user of usersResponse) {
|
|
|
|
|
+ if (user.id) {
|
|
|
|
|
+ tempUsers[user.id] = formatUser(user)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setUsers(tempUsers)
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(error => {
|
|
|
|
|
+ refreshingRangeUsers.current = false
|
|
|
|
|
+ debugConsole.error(error)
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ [projectId, setUsers, user]
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ const getChangeTracker = useCallback(
|
|
|
|
|
+ (docId: DocId) => {
|
|
|
|
|
+ if (!rangesTrackers.current[docId]) {
|
|
|
|
|
+ rangesTrackers.current[docId] = new RangesTracker() as RangesTracker
|
|
|
|
|
+ rangesTrackers.current[docId].resolvedThreadIds = {
|
|
|
|
|
+ ...resolvedThreadIds,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return rangesTrackers.current[docId]
|
|
|
|
|
+ },
|
|
|
|
|
+ [resolvedThreadIds]
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ const getDocEntries = useCallback(
|
|
|
|
|
+ (docId: DocId) => {
|
|
|
|
|
+ return entries[docId] ?? ({} as ReviewPanelDocEntries)
|
|
|
|
|
+ },
|
|
|
|
|
+ [entries]
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ const getDocResolvedComments = useCallback(
|
|
|
|
|
+ (docId: DocId) => {
|
|
|
|
|
+ return resolvedComments[docId] ?? ({} as ReviewPanelDocEntries)
|
|
|
|
|
+ },
|
|
|
|
|
+ [resolvedComments]
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ const updateEntries = useCallback(
|
|
|
|
|
+ async (docId: DocId) => {
|
|
|
|
|
+ const rangesTracker = getChangeTracker(docId)
|
|
|
|
|
+ let localResolvedThreadIds = resolvedThreadIds
|
|
|
|
|
+
|
|
|
|
|
+ if (!isRestrictedTokenMember) {
|
|
|
|
|
+ if (rangesTracker.comments.length > 0) {
|
|
|
|
|
+ const threadsLoadResult = await ensureThreadsAreLoaded()
|
|
|
|
|
+ if (typeof threadsLoadResult === 'object') {
|
|
|
|
|
+ localResolvedThreadIds = threadsLoadResult.resolvedThreadIds
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (loadingThreads) {
|
|
|
|
|
+ // ensure that tracked changes are highlighted even if no comments are loaded
|
|
|
|
|
+ reset()
|
|
|
|
|
+ dispatchReviewPanelEvent('loaded_threads')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const docEntries = cloneDeep(getDocEntries(docId))
|
|
|
|
|
+ const docResolvedComments = cloneDeep(getDocResolvedComments(docId))
|
|
|
|
|
+ // Assume we'll delete everything until we see it, then we'll remove it from this object
|
|
|
|
|
+ const deleteChanges = new Set<keyof ReviewPanelDocEntries>()
|
|
|
|
|
+
|
|
|
|
|
+ for (const [id, change] of Object.entries(docEntries)) {
|
|
|
|
|
+ if (
|
|
|
|
|
+ 'entry_ids' in change &&
|
|
|
|
|
+ id !== 'add-comment' &&
|
|
|
|
|
+ id !== 'bulk-actions'
|
|
|
|
|
+ ) {
|
|
|
|
|
+ for (const entryId of change.entry_ids) {
|
|
|
|
|
+ deleteChanges.add(entryId)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ for (const [, change] of Object.entries(docResolvedComments)) {
|
|
|
|
|
+ if ('entry_ids' in change) {
|
|
|
|
|
+ for (const entryId of change.entry_ids) {
|
|
|
|
|
+ deleteChanges.add(entryId)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let potentialAggregate = false
|
|
|
|
|
+ let prevInsertion = null
|
|
|
|
|
+
|
|
|
|
|
+ for (const change of rangesTracker.changes as any[]) {
|
|
|
|
|
+ if (
|
|
|
|
|
+ potentialAggregate &&
|
|
|
|
|
+ change.op.d &&
|
|
|
|
|
+ change.op.p === prevInsertion.op.p + prevInsertion.op.i.length &&
|
|
|
|
|
+ change.metadata.user_id === prevInsertion.metadata.user_id
|
|
|
|
|
+ ) {
|
|
|
|
|
+ // An actual aggregate op.
|
|
|
|
|
+ const aggregateChangeEntries = docEntries as Record<
|
|
|
|
|
+ string,
|
|
|
|
|
+ ReviewPanelAggregateChangeEntry
|
|
|
|
|
+ >
|
|
|
|
|
+ aggregateChangeEntries[prevInsertion.id].type = 'aggregate-change'
|
|
|
|
|
+ aggregateChangeEntries[prevInsertion.id].metadata.replaced_content =
|
|
|
|
|
+ change.op.d
|
|
|
|
|
+ aggregateChangeEntries[prevInsertion.id].entry_ids.push(change.id)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (docEntries[change.id] == null) {
|
|
|
|
|
+ docEntries[change.id] = {} as ReviewPanelEntry
|
|
|
|
|
+ }
|
|
|
|
|
+ deleteChanges.delete(change.id)
|
|
|
|
|
+ const newEntry: Partial<ReviewPanelChangeEntry> = {
|
|
|
|
|
+ type: change.op.i ? 'insert' : 'delete',
|
|
|
|
|
+ entry_ids: [change.id],
|
|
|
|
|
+ content: change.op.i || change.op.d,
|
|
|
|
|
+ offset: change.op.p,
|
|
|
|
|
+ metadata: change.metadata,
|
|
|
|
|
+ }
|
|
|
|
|
+ const newEntryEntries = Object.entries(newEntry) as [
|
|
|
|
|
+ [keyof typeof newEntry, typeof newEntry[keyof typeof newEntry]]
|
|
|
|
|
+ ]
|
|
|
|
|
+ for (const [key, value] of newEntryEntries) {
|
|
|
|
|
+ const entriesTyped = docEntries[change.id] as Record<any, any>
|
|
|
|
|
+ entriesTyped[key] = value
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (change.op.i) {
|
|
|
|
|
+ potentialAggregate = true
|
|
|
|
|
+ prevInsertion = change
|
|
|
|
|
+ } else {
|
|
|
|
|
+ potentialAggregate = false
|
|
|
|
|
+ prevInsertion = null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!users[change.metadata.user_id]) {
|
|
|
|
|
+ if (!isRestrictedTokenMember) {
|
|
|
|
|
+ refreshChangeUsers(change.metadata.user_id)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const comment of rangesTracker.comments) {
|
|
|
|
|
+ deleteChanges.delete(comment.id)
|
|
|
|
|
+
|
|
|
|
|
+ const newEntry: Partial<ReviewPanelCommentEntry> = {
|
|
|
|
|
+ type: 'comment',
|
|
|
|
|
+ thread_id: comment.op.t,
|
|
|
|
|
+ entry_ids: [comment.id],
|
|
|
|
|
+ content: comment.op.c,
|
|
|
|
|
+ offset: comment.op.p,
|
|
|
|
|
+ }
|
|
|
|
|
+ const newEntryEntries = Object.entries(newEntry) as [
|
|
|
|
|
+ [keyof typeof newEntry, typeof newEntry[keyof typeof newEntry]]
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ let newComment: any
|
|
|
|
|
+ if (localResolvedThreadIds[comment.op.t]) {
|
|
|
|
|
+ docResolvedComments[comment.id] ??= {} as ReviewPanelCommentEntry
|
|
|
|
|
+ newComment = docResolvedComments[comment.id]
|
|
|
|
|
+ delete docEntries[comment.id]
|
|
|
|
|
+ } else {
|
|
|
|
|
+ docEntries[comment.id] ??= {} as ReviewPanelEntry
|
|
|
|
|
+ newComment = docEntries[comment.id]
|
|
|
|
|
+ delete docResolvedComments[comment.id]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const [key, value] of newEntryEntries) {
|
|
|
|
|
+ newComment[key] = value
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ deleteChanges.forEach(changeId => {
|
|
|
|
|
+ delete docEntries[changeId]
|
|
|
|
|
+ delete docResolvedComments[changeId]
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ setEntries(prev => {
|
|
|
|
|
+ return isEqual(prev[docId], docEntries)
|
|
|
|
|
+ ? prev
|
|
|
|
|
+ : { ...prev, [docId]: docEntries }
|
|
|
|
|
+ })
|
|
|
|
|
+ setResolvedComments(prev => {
|
|
|
|
|
+ return isEqual(prev[docId], docResolvedComments)
|
|
|
|
|
+ ? prev
|
|
|
|
|
+ : { ...prev, [docId]: docResolvedComments }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return docEntries
|
|
|
|
|
+ },
|
|
|
|
|
+ [
|
|
|
|
|
+ getChangeTracker,
|
|
|
|
|
+ getDocEntries,
|
|
|
|
|
+ getDocResolvedComments,
|
|
|
|
|
+ isRestrictedTokenMember,
|
|
|
|
|
+ refreshChangeUsers,
|
|
|
|
|
+ resolvedThreadIds,
|
|
|
|
|
+ users,
|
|
|
|
|
+ ensureThreadsAreLoaded,
|
|
|
|
|
+ loadingThreads,
|
|
|
|
|
+ reset,
|
|
|
|
|
+ ]
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ const regenerateTrackChangesId = useCallback(
|
|
|
|
|
+ (doc: typeof currentDocument) => {
|
|
|
|
|
+ const currentChangeTracker = getChangeTracker(doc.doc_id as DocId)
|
|
|
|
|
+ const oldId = currentChangeTracker.getIdSeed()
|
|
|
|
|
+ const newId = RangesTracker.generateIdSeed()
|
|
|
|
|
+ currentChangeTracker.setIdSeed(newId)
|
|
|
|
|
+ doc.setTrackChangesIdSeeds({ pending: newId, inflight: oldId })
|
|
|
|
|
+ },
|
|
|
|
|
+ [getChangeTracker]
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (!currentDocument) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ // The open doc range tracker is kept up to date in real-time so
|
|
|
|
|
+ // replace any outdated info with this
|
|
|
|
|
+ rangesTrackers.current[currentDocument.doc_id as DocId] =
|
|
|
|
|
+ currentDocument.ranges
|
|
|
|
|
+ rangesTrackers.current[currentDocument.doc_id as DocId].resolvedThreadIds =
|
|
|
|
|
+ { ...resolvedThreadIds }
|
|
|
|
|
+ currentDocument.on('flipped_pending_to_inflight', () =>
|
|
|
|
|
+ regenerateTrackChangesId(currentDocument)
|
|
|
|
|
+ )
|
|
|
|
|
+ regenerateTrackChangesId(currentDocument)
|
|
|
|
|
+
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ currentDocument.off('flipped_pending_to_inflight')
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [currentDocument, regenerateTrackChangesId, resolvedThreadIds])
|
|
|
|
|
+
|
|
|
const currentUserType = useCallback((): 'member' | 'guest' | 'anonymous' => {
|
|
const currentUserType = useCallback((): 'member' | 'guest' | 'anonymous' => {
|
|
|
if (!user) {
|
|
if (!user) {
|
|
|
return 'anonymous'
|
|
return 'anonymous'
|
|
@@ -387,6 +748,7 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|
|
const projectJoinedEffectExecuted = useRef(false)
|
|
const projectJoinedEffectExecuted = useRef(false)
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
if (!projectJoinedEffectExecuted.current) {
|
|
if (!projectJoinedEffectExecuted.current) {
|
|
|
|
|
+ projectJoinedEffectExecuted.current = true
|
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
|
if (trackChanges) {
|
|
if (trackChanges) {
|
|
|
applyTrackChangesStateToClient(project.trackChangesState)
|
|
applyTrackChangesStateToClient(project.trackChangesState)
|
|
@@ -395,7 +757,6 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|
|
}
|
|
}
|
|
|
setGuestFeatureBasedOnProjectAccessLevel(project.publicAccessLevel)
|
|
setGuestFeatureBasedOnProjectAccessLevel(project.publicAccessLevel)
|
|
|
})
|
|
})
|
|
|
- projectJoinedEffectExecuted.current = true
|
|
|
|
|
}
|
|
}
|
|
|
}, [
|
|
}, [
|
|
|
applyTrackChangesStateToClient,
|
|
applyTrackChangesStateToClient,
|
|
@@ -489,13 +850,10 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|
|
'bulkRejectActions'
|
|
'bulkRejectActions'
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- const handleSetSubview = useCallback(
|
|
|
|
|
- (subView: SubView) => {
|
|
|
|
|
- setSubView(subView)
|
|
|
|
|
- sendMB('rp-subview-change', { subView })
|
|
|
|
|
- },
|
|
|
|
|
- [setSubView]
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ const handleSetSubview = useCallback((subView: SubView) => {
|
|
|
|
|
+ setSubView(subView)
|
|
|
|
|
+ sendMB('rp-subview-change', { subView })
|
|
|
|
|
+ }, [])
|
|
|
|
|
|
|
|
const submitReply = useCallback(
|
|
const submitReply = useCallback(
|
|
|
(threadId: ThreadId, replyContent: string) => {
|
|
(threadId: ThreadId, replyContent: string) => {
|
|
@@ -523,11 +881,27 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const editorTrackChangesChanged = async () => {
|
|
|
|
|
+ const entries = await updateEntries(currentDocumentId)
|
|
|
|
|
+ dispatchReviewPanelEvent('recalculate-screen-positions', {
|
|
|
|
|
+ entries,
|
|
|
|
|
+ updateType: 'trackedChangesChange',
|
|
|
|
|
+ })
|
|
|
|
|
+ // Ensure that watchers, such as the React-based review panel component,
|
|
|
|
|
+ // are informed of the changes to entries
|
|
|
|
|
+ handleLayoutChange()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const handleEditorEvents = (e: Event) => {
|
|
const handleEditorEvents = (e: Event) => {
|
|
|
const event = e as CustomEvent
|
|
const event = e as CustomEvent
|
|
|
const { type } = event.detail
|
|
const { type } = event.detail
|
|
|
|
|
|
|
|
switch (type) {
|
|
switch (type) {
|
|
|
|
|
+ case 'track-changes:changed': {
|
|
|
|
|
+ editorTrackChangesChanged()
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
case 'toggle-track-changes': {
|
|
case 'toggle-track-changes': {
|
|
|
toggleTrackChangesFromKbdShortcut()
|
|
toggleTrackChangesFromKbdShortcut()
|
|
|
break
|
|
break
|
|
@@ -541,10 +915,12 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|
|
window.removeEventListener('editor:event', handleEditorEvents)
|
|
window.removeEventListener('editor:event', handleEditorEvents)
|
|
|
}
|
|
}
|
|
|
}, [
|
|
}, [
|
|
|
|
|
+ currentDocumentId,
|
|
|
toggleTrackChangesForUser,
|
|
toggleTrackChangesForUser,
|
|
|
trackChanges,
|
|
trackChanges,
|
|
|
trackChangesState,
|
|
trackChangesState,
|
|
|
trackChangesVisible,
|
|
trackChangesVisible,
|
|
|
|
|
+ updateEntries,
|
|
|
user.id,
|
|
user.id,
|
|
|
])
|
|
])
|
|
|
|
|
|