From 8b41eaddb821cd0fd80b7d1c72e4c7ebe1d28f37 Mon Sep 17 00:00:00 2001 From: yu-i-i Date: Tue, 8 Apr 2025 16:49:58 +0200 Subject: [PATCH] Track changes / comments: update backend to support frontend changes --- .../hooks/use-review-panel-state.ts | 1659 ----------------- .../app/src/TrackChangesController.js | 61 +- 2 files changed, 22 insertions(+), 1698 deletions(-) delete mode 100644 services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts diff --git a/services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts b/services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts deleted file mode 100644 index c712384163..0000000000 --- a/services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts +++ /dev/null @@ -1,1659 +0,0 @@ -import { useState, useEffect, useMemo, useCallback, useRef } from 'react' -import { useTranslation } from 'react-i18next' -import { isEqual, cloneDeep } from 'lodash' -import usePersistedState from '@/shared/hooks/use-persisted-state' -import useScopeValue from '../../../../../shared/hooks/use-scope-value' -import useSocketListener from '@/features/ide-react/hooks/use-socket-listener' -import useAbortController from '@/shared/hooks/use-abort-controller' -import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter' -import useLayoutToLeft from '@/features/ide-react/context/review-panel/hooks/useLayoutToLeft' -import { sendMB } from '@/infrastructure/event-tracking' -import { - dispatchReviewPanelLayout as handleLayoutChange, - UpdateType, -} from '@/features/source-editor/extensions/changes/change-manager' -import { useProjectContext } from '@/shared/context/project-context' -import { useLayoutContext } from '@/shared/context/layout-context' -import { useUserContext } from '@/shared/context/user-context' -import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context' -import { useConnectionContext } from '@/features/ide-react/context/connection-context' -import { usePermissionsContext } from '@/features/ide-react/context/permissions-context' -import { useModalsContext } from '@/features/ide-react/context/modals-context' -import { - EditorManager, - useEditorManagerContext, -} from '@/features/ide-react/context/editor-manager-context' -import { debugConsole } from '@/utils/debugging' -import { deleteJSON, getJSON, postJSON } from '@/infrastructure/fetch-json' -import RangesTracker from '@overleaf/ranges-tracker' -import type * as ReviewPanel from '@/features/source-editor/context/review-panel/types/review-panel-state' -import { - CommentId, - ReviewPanelCommentThreadMessage, - ReviewPanelCommentThreads, - ReviewPanelDocEntries, - SubView, - ThreadId, -} from '../../../../../../../types/review-panel/review-panel' -import { UserId } from '../../../../../../../types/user' -import { PublicAccessLevel } from '../../../../../../../types/public-access-level' -import { - DeepReadonly, - Entries, - MergeAndOverride, -} from '../../../../../../../types/utils' -import { ReviewPanelCommentThread } from '../../../../../../../types/review-panel/comment-thread' -import { DocId } from '../../../../../../../types/project-settings' -import { - ReviewPanelAddCommentEntry, - ReviewPanelAggregateChangeEntry, - ReviewPanelBulkActionsEntry, - ReviewPanelChangeEntry, - ReviewPanelCommentEntry, - ReviewPanelEntry, -} from '../../../../../../../types/review-panel/entry' -import { - ReviewPanelCommentThreadMessageApi, - ReviewPanelCommentThreadsApi, -} from '../../../../../../../types/review-panel/api' -import { DateString } from '../../../../../../../types/helpers/date' -import { - Change, - CommentOperation, - EditOperation, -} from '../../../../../../../types/change' -import { RangesTrackerWithResolvedThreadIds } from '@/features/ide-react/editor/document-container' -import getMeta from '@/utils/meta' -import { useEditorContext } from '@/shared/context/editor-context' -import { getHueForUserId } from '@/shared/utils/colors' - -const dispatchReviewPanelEvent = (type: string, payload?: any) => { - window.dispatchEvent( - new CustomEvent('review-panel:event', { - detail: { type, payload }, - }) - ) -} - -const formatUser = (user: any): any => { - let isSelf, name - const id = - (user != null ? user._id : undefined) || - (user != null ? user.id : undefined) - - if (id == null) { - return { - id: 'anonymous-user', - email: null, - name: 'Anonymous', - isSelf: false, - hue: getHueForUserId(), - avatar_text: 'A', - } - } - if (id === getMeta('ol-user_id')) { - name = 'You' - isSelf = true - } else { - name = [user.first_name, user.last_name] - .filter(n => n != null && n !== '') - .join(' ') - if (name === '') { - name = - (user.email != null ? user.email.split('@')[0] : undefined) || 'Unknown' - } - isSelf = false - } - return { - id, - email: user.email, - name, - isSelf, - hue: getHueForUserId(id), - avatar_text: [user.first_name, user.last_name] - .filter(n => n != null) - .map(n => n[0]) - .join(''), - } -} - -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(): ReviewPanel.ReviewPanelState { - const { t } = useTranslation() - const { reviewPanelOpen, setReviewPanelOpen, setMiniReviewPanelVisible } = - useLayoutContext() - const { projectId } = useIdeReactContext() - const project = useProjectContext() - const user = useUserContext() - const { socket } = useConnectionContext() - const { - features: { trackChangesVisible, trackChanges }, - } = project - const { isRestrictedTokenMember } = useEditorContext() - const { - openDocWithId, - currentDocument, - currentDocumentId, - wantTrackChanges, - setWantTrackChanges, - } = useEditorManagerContext() as MergeAndOverride< - EditorManager, - { currentDocumentId: DocId } - > - // TODO permissions to be removed from the review panel context. It currently acts just as a proxy. - const permissions = usePermissionsContext() - const { showGenericMessageModal } = useModalsContext() - const addCommentEmitter = useScopeEventEmitter('comment:start_adding') - - const layoutToLeft = useLayoutToLeft('.ide-react-editor-panel') - const [subView, setSubView] = - useState>('cur_file') - const [isOverviewLoading, setIsOverviewLoading] = - useState>(false) - // All selected changes. If an aggregated change (insertion + deletion) is selected, the two ids - // will be present. The length of this array will differ from the count below (see explanation). - const selectedEntryIds = useRef([]) - // A count of user-facing selected changes. An aggregated change (insertion + deletion) will count - // as only one. - const [nVisibleSelectedChanges, setNVisibleSelectedChanges] = - useState>(0) - const [collapsed, setCollapsed] = usePersistedState< - ReviewPanel.Value<'collapsed'> - >(`docs_collapsed_state:${projectId}`, {}, false, true) - const [commentThreads, setCommentThreads] = useState< - ReviewPanel.Value<'commentThreads'> - >({}) - const [entries, setEntries] = useState>({}) - const [users, setUsers] = useScopeValue>('users') - const [resolvedComments, setResolvedComments] = useState< - ReviewPanel.Value<'resolvedComments'> - >({}) - - const [shouldCollapse, setShouldCollapse] = - useState>(true) - const [lineHeight, setLineHeight] = - useState>(0) - - const [formattedProjectMembers, setFormattedProjectMembers] = useState< - ReviewPanel.Value<'formattedProjectMembers'> - >({}) - const [trackChangesState, setTrackChangesState] = useState< - ReviewPanel.Value<'trackChangesState'> - >({}) - const [trackChangesOnForEveryone, setTrackChangesOnForEveryone] = - useState>(false) - const [trackChangesOnForGuests, setTrackChangesOnForGuests] = - useState>(false) - const [trackChangesForGuestsAvailable, setTrackChangesForGuestsAvailable] = - useState>(false) - - const [resolvedThreadIds, setResolvedThreadIds] = useState< - Record - >({}) - - const [loadingThreads, setLoadingThreads] = - useScopeValue('loadingThreads') - - const loadThreadsController = useAbortController() - const threadsLoadedOnceRef = useRef(false) - const loadingThreadsInProgressRef = useRef(false) - const ensureThreadsAreLoaded = useCallback(() => { - if (threadsLoadedOnceRef.current) { - // We get any updates in real time so only need to load them once. - return - } - threadsLoadedOnceRef.current = true - loadingThreadsInProgressRef.current = true - - return getJSON(`/project/${projectId}/threads`, { - signal: loadThreadsController.signal, - }) - .then(threads => { - setLoadingThreads(false) - 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) - .finally(() => { - loadingThreadsInProgressRef.current = false - }) - }, [loadThreadsController.signal, projectId, setLoadingThreads]) - - const rangesTrackers = useRef< - Record - >({}) - const refreshingRangeUsers = useRef(false) - const refreshedForUserIds = useRef(new Set()) - 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) { - if (user.id) { - tempUsers[user.id] = formatUser(user) - } else { - tempUsers['anonymous-user'] = formatUser(user) - } - } - for (const user of usersResponse) { - if (user.id) { - tempUsers[user.id] = formatUser(user) - } else { - tempUsers['anonymous-user'] = formatUser(user) - } - } - setUsers(tempUsers) - }) - .catch(error => { - refreshingRangeUsers.current = false - debugConsole.error(error) - }) - }, - [projectId, setUsers, user] - ) - - const getChangeTracker = useCallback( - (docId: DocId) => { - if (!rangesTrackers.current[docId]) { - const rangesTracker = new RangesTracker([], []) - ;( - rangesTracker as RangesTrackerWithResolvedThreadIds - ).resolvedThreadIds = { ...resolvedThreadIds } - rangesTrackers.current[docId] = - rangesTracker as RangesTrackerWithResolvedThreadIds - } - 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 getThread = useCallback( - (threadId: ThreadId) => { - return ( - commentThreads[threadId] ?? - ({ messages: [] } as ReviewPanelCommentThread) - ) - }, - [commentThreads] - ) - - const updateEntries = useCallback( - async (docId: DocId) => { - const rangesTracker = getChangeTracker(docId) - 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() - - for (const [id, change] of Object.entries(docEntries) as Entries< - typeof 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) as Entries< - typeof 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 = { - type: change.op.i ? 'insert' : 'delete', - entry_ids: [change.id], - content: change.op.i || change.op.d, - offset: change.op.p, - metadata: change.metadata, - } - for (const [key, value] of Object.entries(newEntry) as Entries< - typeof newEntry - >) { - const entriesTyped = docEntries[change.id] as Record - 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) - } - } - } - - let localResolvedThreadIds = resolvedThreadIds - - if (!isRestrictedTokenMember && rangesTracker.comments.length > 0) { - const threadsLoadResult = await ensureThreadsAreLoaded() - if (threadsLoadResult?.resolvedThreadIds) { - localResolvedThreadIds = threadsLoadResult.resolvedThreadIds - } - } else if (loadingThreads) { - // ensure that tracked changes are highlighted even if no comments are loaded - setLoadingThreads(false) - dispatchReviewPanelEvent('loaded_threads') - } - - if (!loadingThreadsInProgressRef.current) { - for (const comment of rangesTracker.comments) { - const commentId = comment.id as ThreadId - deleteChanges.delete(commentId) - - let newComment: any - if (localResolvedThreadIds[comment.op.t]) { - docResolvedComments[commentId] ??= {} as ReviewPanelCommentEntry - newComment = docResolvedComments[commentId] - delete docEntries[commentId] - } else { - docEntries[commentId] ??= {} as ReviewPanelEntry - newComment = docEntries[commentId] - delete docResolvedComments[commentId] - } - - newComment.type = 'comment' - newComment.thread_id = comment.op.t - newComment.entry_ids = [comment.id] - newComment.content = comment.op.c - newComment.offset = comment.op.p - } - } - - 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, - refreshChangeUsers, - resolvedThreadIds, - users, - ensureThreadsAreLoaded, - loadingThreads, - setLoadingThreads, - isRestrictedTokenMember, - ] - ) - - const regenerateTrackChangesId = useCallback( - (doc: typeof currentDocument) => { - if (doc) { - 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 - const rangesTracker = currentDocument.ranges! - ;(rangesTracker as RangesTrackerWithResolvedThreadIds).resolvedThreadIds = { - ...resolvedThreadIds, - } - rangesTrackers.current[currentDocument.doc_id as DocId] = - rangesTracker as RangesTrackerWithResolvedThreadIds - 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-user' => { - if (!user) { - return 'anonymous-user' - } - if (project.owner._id === user.id) { - return 'member' - } - for (const member of project.members as any[]) { - if (member._id === user.id) { - return 'member' - } - } - return 'guest' - }, [project.members, project.owner, user]) - - const applyClientTrackChangesStateToServer = useCallback( - ( - trackChangesOnForEveryone: boolean, - trackChangesOnForGuests: boolean, - trackChangesState: ReviewPanel.Value<'trackChangesState'> - ) => { - const data: { - on?: boolean - on_for?: Record - on_for_guests?: boolean - } = {} - if (trackChangesOnForEveryone) { - data.on = true - } else { - data.on_for = {} - const entries = Object.entries(trackChangesState) as Array< - [ - UserId, - NonNullable< - (typeof trackChangesState)[keyof typeof trackChangesState] - >, - ] - > - for (const [userId, { value }] of entries) { - data.on_for[userId] = value - } - if (trackChangesOnForGuests) { - data.on_for_guests = true - } - } - postJSON(`/project/${projectId}/track_changes`, { - body: data, - }).catch(debugConsole.error) - }, - [projectId] - ) - - const setGuestsTCState = useCallback( - (newValue: boolean) => { - setTrackChangesOnForGuests(newValue) - if (currentUserType() === 'guest' || currentUserType() === 'anonymous-user') { - setWantTrackChanges(newValue) - } - }, - [currentUserType, setWantTrackChanges] - ) - - const setUserTCState = useCallback( - ( - trackChangesState: DeepReadonly>, - userId: UserId, - newValue: boolean, - isLocal = false - ) => { - const newTrackChangesState: ReviewPanel.Value<'trackChangesState'> = { - ...trackChangesState, - } - const state = - newTrackChangesState[userId] ?? - ({} as NonNullable<(typeof newTrackChangesState)[UserId]>) - newTrackChangesState[userId] = state - - if (state.syncState == null || state.syncState === 'synced') { - state.value = newValue - state.syncState = 'synced' - } else if (state.syncState === 'pending' && state.value === newValue) { - state.syncState = 'synced' - } else if (isLocal) { - state.value = newValue - state.syncState = 'pending' - } - - setTrackChangesState(newTrackChangesState) - - if (userId === user.id) { - setWantTrackChanges(newValue) - } - - return newTrackChangesState - }, - [setWantTrackChanges, user.id] - ) - - const setEveryoneTCState = useCallback( - (newValue: boolean, isLocal = false) => { - setTrackChangesOnForEveryone(newValue) - let newTrackChangesState: ReviewPanel.Value<'trackChangesState'> = { - ...trackChangesState, - } - for (const member of project.members as any[]) { - newTrackChangesState = setUserTCState( - newTrackChangesState, - member._id, - newValue, - isLocal - ) - } - setGuestsTCState(newValue) - - newTrackChangesState = setUserTCState( - newTrackChangesState, - project.owner._id, - newValue, - isLocal - ) - - return { trackChangesState: newTrackChangesState } - }, - [ - project.members, - project.owner._id, - setGuestsTCState, - setUserTCState, - trackChangesState, - ] - ) - - const toggleTrackChangesForEveryone = useCallback< - ReviewPanel.UpdaterFn<'toggleTrackChangesForEveryone'> - >( - (onForEveryone: boolean) => { - const { trackChangesState } = setEveryoneTCState(onForEveryone, true) - setGuestsTCState(onForEveryone) - applyClientTrackChangesStateToServer( - onForEveryone, - onForEveryone, - trackChangesState - ) - }, - [applyClientTrackChangesStateToServer, setEveryoneTCState, setGuestsTCState] - ) - - const toggleTrackChangesForGuests = useCallback< - ReviewPanel.UpdaterFn<'toggleTrackChangesForGuests'> - >( - (onForGuests: boolean) => { - setGuestsTCState(onForGuests) - applyClientTrackChangesStateToServer( - trackChangesOnForEveryone, - onForGuests, - trackChangesState - ) - }, - [ - applyClientTrackChangesStateToServer, - setGuestsTCState, - trackChangesOnForEveryone, - trackChangesState, - ] - ) - - const toggleTrackChangesForUser = useCallback< - ReviewPanel.UpdaterFn<'toggleTrackChangesForUser'> - >( - (onForUser: boolean, userId: UserId) => { - const newTrackChangesState = setUserTCState( - trackChangesState, - userId, - onForUser, - true - ) - applyClientTrackChangesStateToServer( - trackChangesOnForEveryone, - trackChangesOnForGuests, - newTrackChangesState - ) - }, - [ - applyClientTrackChangesStateToServer, - setUserTCState, - trackChangesOnForEveryone, - trackChangesOnForGuests, - trackChangesState, - ] - ) - - const applyTrackChangesStateToClient = useCallback( - (state: boolean | Record) => { - if (typeof state === 'boolean') { - setEveryoneTCState(state) - setGuestsTCState(state) - } else { - setTrackChangesOnForEveryone(false) - // TODO - // @ts-ignore - setGuestsTCState(state.__guests__ === true) - - let newTrackChangesState: ReviewPanel.Value<'trackChangesState'> = { - ...trackChangesState, - } - for (const member of project.members as any[]) { - newTrackChangesState = setUserTCState( - newTrackChangesState, - member._id, - !!state[member._id] - ) - } - newTrackChangesState = setUserTCState( - newTrackChangesState, - project.owner._id, - !!state[project.owner._id] - ) - return newTrackChangesState - } - }, - [ - project.members, - project.owner._id, - setEveryoneTCState, - setGuestsTCState, - setUserTCState, - trackChangesState, - ] - ) - - const setGuestFeatureBasedOnProjectAccessLevel = ( - projectPublicAccessLevel?: PublicAccessLevel - ) => { - setTrackChangesForGuestsAvailable(projectPublicAccessLevel === 'tokenBased') - } - - useEffect(() => { - setGuestFeatureBasedOnProjectAccessLevel(project.publicAccessLevel) - }, [project.publicAccessLevel]) - - useEffect(() => { - if ( - trackChangesForGuestsAvailable || - !trackChangesOnForGuests || - trackChangesOnForEveryone - ) { - return - } - - // Overrides guest setting - toggleTrackChangesForGuests(false) - }, [ - toggleTrackChangesForGuests, - trackChangesForGuestsAvailable, - trackChangesOnForEveryone, - trackChangesOnForGuests, - ]) - - const projectJoinedEffectExecuted = useRef(false) - useEffect(() => { - if (!projectJoinedEffectExecuted.current) { - projectJoinedEffectExecuted.current = true - requestAnimationFrame(() => { - if (trackChanges) { - applyTrackChangesStateToClient(project.trackChangesState) - } else { - applyTrackChangesStateToClient(false) - } - setGuestFeatureBasedOnProjectAccessLevel(project.publicAccessLevel) - }) - } - }, [ - applyTrackChangesStateToClient, - trackChanges, - project.publicAccessLevel, - project.trackChangesState, - ]) - - useEffect(() => { - setFormattedProjectMembers(prevState => { - const tempFormattedProjectMembers: typeof prevState = {} - if (project.owner) { - tempFormattedProjectMembers[project.owner._id] = formatUser( - project.owner - ) - } - const members = project.members ?? [] - for (const member of members) { - if (member.privileges === 'readAndWrite') { - if (!trackChangesState[member._id]) { - // An added member will have track changes enabled if track changes is on for everyone - setUserTCState( - trackChangesState, - member._id, - trackChangesOnForEveryone, - true - ) - } - tempFormattedProjectMembers[member._id] = formatUser(member) - } - } - return tempFormattedProjectMembers - }) - }, [ - project.members, - project.owner, - setUserTCState, - trackChangesOnForEveryone, - trackChangesState, - ]) - - useSocketListener( - socket, - 'toggle-track-changes', - applyTrackChangesStateToClient - ) - - const gotoEntry = useCallback( - (docId: DocId, entryOffset: number) => { - openDocWithId(docId, { gotoOffset: entryOffset }) - }, - [openDocWithId] - ) - - const view = reviewPanelOpen ? subView : 'mini' - - const toggleReviewPanel = useCallback(() => { - if (!trackChangesVisible) { - return - } - setReviewPanelOpen(!reviewPanelOpen) - sendMB('rp-toggle-panel', { - value: !reviewPanelOpen, - }) - }, [reviewPanelOpen, setReviewPanelOpen, trackChangesVisible]) - - const onCommentResolved = useCallback( - (threadId: ThreadId, user: any) => { - setCommentThreads(prevState => { - const thread = { ...getThread(threadId) } - thread.resolved = true - thread.resolved_by_user = formatUser(user) - thread.resolved_at = new Date().toISOString() as DateString - return { ...prevState, [threadId]: thread } - }) - setResolvedThreadIds(prevState => ({ ...prevState, [threadId]: true })) - setTimeout(() => { - dispatchReviewPanelEvent('comment:resolve_threads', [threadId]) - }) - }, - [getThread] - ) - - const resolveComment = useCallback( - (docId: DocId, entryId: ThreadId) => { - const docEntries = getDocEntries(docId) - const entry = docEntries[entryId] as ReviewPanelCommentEntry - - setEntries(prevState => ({ - ...prevState, - [docId]: { - ...prevState[docId], - [entryId]: { - ...prevState[docId][entryId], - focused: false, - }, - }, - })) - - postJSON( - `/project/${projectId}/doc/${docId}/thread/${entry.thread_id}/resolve` - ) - onCommentResolved(entry.thread_id, user) - sendMB('rp-comment-resolve', { view }) - }, - [getDocEntries, onCommentResolved, projectId, user, view] - ) - - const onCommentReopened = useCallback( - (threadId: ThreadId) => { - setCommentThreads(prevState => { - const { - resolved: _1, - resolved_by_user: _2, - resolved_at: _3, - ...thread - } = getThread(threadId) - return { ...prevState, [threadId]: thread } - }) - setResolvedThreadIds(({ [threadId]: _, ...resolvedThreadIds }) => { - return resolvedThreadIds - }) - setTimeout(() => { - dispatchReviewPanelEvent('comment:unresolve_thread', threadId) - }) - }, - [getThread] - ) - - const unresolveComment = useCallback( - (docId: DocId, threadId: ThreadId) => { - onCommentReopened(threadId) - const url = `/project/${projectId}/doc/${docId}/thread/${threadId}/reopen` - postJSON(url).catch(debugConsole.error) - sendMB('rp-comment-reopen') - }, - [onCommentReopened, projectId] - ) - - const onThreadDeleted = useCallback((threadId: ThreadId) => { - setResolvedThreadIds(({ [threadId]: _, ...resolvedThreadIds }) => { - return resolvedThreadIds - }) - setCommentThreads(({ [threadId]: _, ...commentThreads }) => { - return commentThreads - }) - dispatchReviewPanelEvent('comment:remove', threadId) - }, []) - - const deleteThread = useCallback( - (docId: DocId, threadId: ThreadId) => { - onThreadDeleted(threadId) - deleteJSON(`/project/${projectId}/doc/${docId}/thread/${threadId}`).catch( - debugConsole.error - ) - sendMB('rp-comment-delete') - }, - [onThreadDeleted, projectId] - ) - - const onCommentEdited = useCallback( - (threadId: ThreadId, commentId: CommentId, content: string) => { - setCommentThreads(prevState => { - const thread = { ...getThread(threadId) } - thread.messages = thread.messages.map(message => { - return message.id === commentId ? { ...message, content } : message - }) - return { ...prevState, [threadId]: thread } - }) - }, - [getThread] - ) - - const saveEdit = useCallback( - (threadId: ThreadId, commentId: CommentId, content: string) => { - const url = `/project/${projectId}/thread/${threadId}/messages/${commentId}/edit` - postJSON(url, { body: { content } }).catch(debugConsole.error) - handleLayoutChange({ async: true }) - }, - [projectId] - ) - - const onCommentDeleted = useCallback( - (threadId: ThreadId, commentId: CommentId) => { - setCommentThreads(prevState => { - const thread = { ...getThread(threadId) } - thread.messages = thread.messages.filter(m => m.id !== commentId) - return { ...prevState, [threadId]: thread } - }) - }, - [getThread] - ) - - const deleteComment = useCallback( - (threadId: ThreadId, commentId: CommentId) => { - onCommentDeleted(threadId, commentId) - deleteJSON( - `/project/${projectId}/thread/${threadId}/messages/${commentId}` - ).catch(debugConsole.error) - handleLayoutChange({ async: true }) - }, - [onCommentDeleted, projectId] - ) - - const doAcceptChanges = useCallback( - (entryIds: ThreadId[]) => { - const url = `/project/${projectId}/doc/${currentDocumentId}/changes/accept` - postJSON(url, { body: { change_ids: entryIds } }).catch( - debugConsole.error - ) - dispatchReviewPanelEvent('changes:accept', entryIds) - }, - [currentDocumentId, projectId] - ) - - const acceptChanges = useCallback( - (entryIds: ThreadId[]) => { - doAcceptChanges(entryIds) - sendMB('rp-changes-accepted', { view }) - }, - [doAcceptChanges, view] - ) - - const doRejectChanges = useCallback((entryIds: ThreadId[]) => { - dispatchReviewPanelEvent('changes:reject', entryIds) - }, []) - - const rejectChanges = useCallback( - (entryIds: ThreadId[]) => { - doRejectChanges(entryIds) - sendMB('rp-changes-rejected', { view }) - }, - [doRejectChanges, view] - ) - - const bulkAcceptActions = useCallback(() => { - doAcceptChanges(selectedEntryIds.current) - sendMB('rp-bulk-accept', { view, nEntries: nVisibleSelectedChanges }) - }, [doAcceptChanges, nVisibleSelectedChanges, view]) - - const bulkRejectActions = useCallback(() => { - doRejectChanges(selectedEntryIds.current) - sendMB('rp-bulk-reject', { view, nEntries: nVisibleSelectedChanges }) - }, [doRejectChanges, nVisibleSelectedChanges, view]) - - const refreshRanges = useCallback(() => { - type Doc = { - id: DocId - ranges: { - comments?: Change[] - changes?: Change[] - } - } - - return getJSON(`/project/${projectId}/ranges`) - .then(docs => { - setCollapsed(prevState => { - const collapsed = { ...prevState } - docs.forEach(doc => { - if (collapsed[doc.id] == null) { - collapsed[doc.id] = false - } - }) - return collapsed - }) - - docs.forEach(async doc => { - if (doc.id !== currentDocumentId) { - // this is kept up to date in real-time, don't overwrite - const rangesTracker = getChangeTracker(doc.id) - rangesTracker.comments = doc.ranges?.comments ?? [] - rangesTracker.changes = doc.ranges?.changes ?? [] - } - }) - - return Promise.all(docs.map(doc => updateEntries(doc.id))) - }) - .catch(debugConsole.error) - }, [ - currentDocumentId, - getChangeTracker, - projectId, - setCollapsed, - updateEntries, - ]) - - const handleSetSubview = useCallback((subView: SubView) => { - setSubView(subView) - sendMB('rp-subview-change', { subView }) - }, []) - - const submitReply = useCallback( - (threadId: ThreadId, replyContent: string) => { - const url = `/project/${projectId}/thread/${threadId}/messages` - postJSON(url, { body: { content: replyContent } }).catch(() => { - showGenericMessageModal( - t('error_submitting_comment'), - t('comment_submit_error') - ) - }) - - const trackingMetadata = { - view, - size: replyContent.length, - thread: threadId, - } - - setCommentThreads(prevState => ({ - ...prevState, - [threadId]: { ...getThread(threadId), submitting: true }, - })) - handleLayoutChange({ async: true }) - sendMB('rp-comment-reply', trackingMetadata) - }, - [getThread, projectId, showGenericMessageModal, t, view] - ) - - // TODO `submitNewComment` is partially localized in the `add-comment-entry` component. - const submitNewComment = useCallback( - (content: string) => { - if (!content) { - return - } - - const entries = getDocEntries(currentDocumentId) - const addCommentEntry = entries['add-comment'] as - | ReviewPanelAddCommentEntry - | undefined - - if (!addCommentEntry) { - return - } - - const { offset, length } = addCommentEntry - const threadId = RangesTracker.generateId() as ThreadId - setCommentThreads(prevState => ({ - ...prevState, - [threadId]: { ...getThread(threadId), submitting: true }, - })) - - const url = `/project/${projectId}/thread/${threadId}/messages` - postJSON(url, { body: { content } }) - .then(() => { - dispatchReviewPanelEvent('comment:add', { threadId, offset, length }) - handleLayoutChange({ async: true }) - sendMB('rp-new-comment', { size: content.length }) - }) - .catch(() => { - showGenericMessageModal( - t('error_submitting_comment'), - t('comment_submit_error') - ) - }) - }, - [ - currentDocumentId, - getDocEntries, - getThread, - projectId, - showGenericMessageModal, - t, - ] - ) - - const [isAddingComment, setIsAddingComment] = useState(false) - const [navHeight, setNavHeight] = useState(0) - const [toolbarHeight, setToolbarHeight] = useState(0) - const [layoutSuspended, setLayoutSuspended] = useState(false) - const [unsavedComment, setUnsavedComment] = useState('') - - useEffect(() => { - if (!trackChangesVisible) { - setReviewPanelOpen(false) - } - }, [trackChangesVisible, setReviewPanelOpen]) - - const hasEntries = useMemo(() => { - const docEntries = getDocEntries(currentDocumentId) - const permEntriesCount = Object.keys(docEntries).filter(key => { - return !['add-comment', 'bulk-actions'].includes(key) - }).length - return permEntriesCount > 0 && trackChangesVisible - }, [currentDocumentId, getDocEntries, trackChangesVisible]) - - useEffect(() => { - setMiniReviewPanelVisible(!reviewPanelOpen && !!hasEntries) - }, [reviewPanelOpen, hasEntries, setMiniReviewPanelVisible]) - - // listen for events from the CodeMirror 6 track changes extension - useEffect(() => { - const toggleTrackChangesFromKbdShortcut = () => { - const userId = user.id - if (trackChangesVisible && trackChanges && userId) { - const state = trackChangesState[userId] - if (state) { - toggleTrackChangesForUser(!state.value, userId) - } - } - } - - const editorLineHeightChanged = (payload: typeof lineHeight) => { - setLineHeight(payload) - handleLayoutChange() - } - - const editorTrackChangesChanged = async () => { - const tempEntries = cloneDeep(await updateEntries(currentDocumentId)) - - // `tempEntries` would be mutated - dispatchReviewPanelEvent('recalculate-screen-positions', { - entries: tempEntries, - updateType: 'trackedChangesChange', - }) - - // The state should be updated after dispatching the 'recalculate-screen-positions' - // event as `tempEntries` will be mutated - setEntries(prev => ({ ...prev, [currentDocumentId]: tempEntries })) - handleLayoutChange() - } - - const editorTrackChangesVisibilityChanged = () => { - handleLayoutChange({ async: true, animate: false }) - } - - const editorFocusChanged = ( - selectionOffsetStart: number, - selectionOffsetEnd: number, - selection: boolean, - updateType: UpdateType - ) => { - let tempEntries = cloneDeep(getDocEntries(currentDocumentId)) - // All selected changes will be added to this array. - selectedEntryIds.current = [] - // Count of user-visible changes, i.e. an aggregated change will count as one. - let tempNVisibleSelectedChanges = 0 - - const offset = selectionOffsetStart - const length = selectionOffsetEnd - selectionOffsetStart - - // Recreate the add comment and bulk actions entries only when - // necessary. This is to avoid the UI thinking that these entries have - // changed and getting into an infinite loop. - if (selection) { - const existingAddComment = tempEntries[ - 'add-comment' - ] as ReviewPanelAddCommentEntry - if ( - !existingAddComment || - existingAddComment.offset !== offset || - existingAddComment.length !== length - ) { - tempEntries['add-comment'] = { - type: 'add-comment', - offset, - length, - } as ReviewPanelAddCommentEntry - } - const existingBulkActions = tempEntries[ - 'bulk-actions' - ] as ReviewPanelBulkActionsEntry - if ( - !existingBulkActions || - existingBulkActions.offset !== offset || - existingBulkActions.length !== length - ) { - tempEntries['bulk-actions'] = { - type: 'bulk-actions', - offset, - length, - } as ReviewPanelBulkActionsEntry - } - } else { - delete (tempEntries as Partial)['add-comment'] - delete (tempEntries as Partial)['bulk-actions'] - } - - for (const [key, entry] of Object.entries(tempEntries) as Entries< - typeof tempEntries - >) { - let isChangeEntryAndWithinSelection = false - if (entry.type === 'comment' && !resolvedThreadIds[entry.thread_id]) { - tempEntries = { - ...tempEntries, - [key]: { - ...tempEntries[key], - focused: - entry.offset <= selectionOffsetStart && - selectionOffsetStart <= entry.offset + entry.content.length, - }, - } - } else if ( - entry.type === 'insert' || - entry.type === 'aggregate-change' - ) { - isChangeEntryAndWithinSelection = - entry.offset >= selectionOffsetStart && - entry.offset + entry.content.length <= selectionOffsetEnd - tempEntries = { - ...tempEntries, - [key]: { - ...tempEntries[key], - focused: - entry.offset <= selectionOffsetStart && - selectionOffsetStart <= entry.offset + entry.content.length, - }, - } - } else if (entry.type === 'delete') { - isChangeEntryAndWithinSelection = - selectionOffsetStart <= entry.offset && - entry.offset <= selectionOffsetEnd - tempEntries = { - ...tempEntries, - [key]: { - ...tempEntries[key], - focused: entry.offset === selectionOffsetStart, - }, - } - } else if ( - ['add-comment', 'bulk-actions'].includes(entry.type) && - selection - ) { - tempEntries = { - ...tempEntries, - [key]: { ...tempEntries[key], focused: true }, - } - } - if (isChangeEntryAndWithinSelection) { - const entryIds = 'entry_ids' in entry ? entry.entry_ids : [] - for (const entryId of entryIds) { - selectedEntryIds.current.push(entryId) - } - tempNVisibleSelectedChanges++ - } - } - - // `tempEntries` would be mutated - dispatchReviewPanelEvent('recalculate-screen-positions', { - entries: tempEntries, - updateType, - }) - - // The state should be updated after dispatching the 'recalculate-screen-positions' - // event as `tempEntries` will be mutated - setEntries(prev => ({ ...prev, [currentDocumentId]: tempEntries })) - setNVisibleSelectedChanges(tempNVisibleSelectedChanges) - - handleLayoutChange() - } - - const addNewCommentFromKbdShortcut = () => { - if (!trackChangesVisible) { - return - } - dispatchReviewPanelEvent('comment:select_line') - - if (!reviewPanelOpen) { - toggleReviewPanel() - } - handleLayoutChange({ async: true }) - addCommentEmitter() - } - - const handleEditorEvents = (e: Event) => { - const event = e as CustomEvent - const { type, payload } = event.detail - - switch (type) { - case 'line-height': { - editorLineHeightChanged(payload) - break - } - - case 'track-changes:changed': { - editorTrackChangesChanged() - break - } - - case 'track-changes:visibility_changed': { - editorTrackChangesVisibilityChanged() - break - } - - case 'focus:changed': { - const { from, to, empty, updateType } = payload - editorFocusChanged(from, to, !empty, updateType) - break - } - - case 'add-new-comment': { - addNewCommentFromKbdShortcut() - break - } - - case 'toggle-track-changes': { - toggleTrackChangesFromKbdShortcut() - break - } - - case 'toggle-review-panel': { - toggleReviewPanel() - break - } - } - } - - window.addEventListener('editor:event', handleEditorEvents) - - return () => { - window.removeEventListener('editor:event', handleEditorEvents) - } - }, [ - addCommentEmitter, - currentDocumentId, - getDocEntries, - resolvedThreadIds, - reviewPanelOpen, - toggleReviewPanel, - toggleTrackChangesForUser, - trackChanges, - trackChangesState, - trackChangesVisible, - updateEntries, - user.id, - ]) - - useSocketListener(socket, 'reopen-thread', onCommentReopened) - useSocketListener(socket, 'delete-thread', onThreadDeleted) - useSocketListener(socket, 'resolve-thread', onCommentResolved) - useSocketListener(socket, 'edit-message', onCommentEdited) - useSocketListener(socket, 'delete-message', onCommentDeleted) - useSocketListener( - socket, - 'accept-changes', - useCallback( - (docId: DocId, entryIds: ThreadId[]) => { - if (docId !== currentDocumentId) { - getChangeTracker(docId).removeChangeIds(entryIds) - } else { - dispatchReviewPanelEvent('changes:accept', entryIds) - } - updateEntries(docId) - }, - [currentDocumentId, getChangeTracker, updateEntries] - ) - ) - useSocketListener( - socket, - 'new-comment', - useCallback( - (threadId: ThreadId, comment: ReviewPanelCommentThreadMessageApi) => { - setCommentThreads(prevState => { - const { submitting: _, ...thread } = getThread(threadId) - thread.messages = [...thread.messages] - thread.messages.push(formatComment(comment)) - return { ...prevState, [threadId]: thread } - }) - handleLayoutChange({ async: true }) - }, - [getThread] - ) - ) - useSocketListener( - socket, - 'new-comment-threads', - useCallback( - (threads: ReviewPanelCommentThreadsApi) => { - setCommentThreads(prevState => { - const newThreads = { ...prevState } - for (const threadIdString of Object.keys(threads)) { - const threadId = threadIdString as ThreadId - const { submitting: _, ...thread } = getThread(threadId) - // Replace already loaded messages with the server provided ones - thread.messages = threads[threadId].messages.map(formatComment) - newThreads[threadId] = thread - } - return newThreads - }) - handleLayoutChange({ async: true }) - }, - [getThread] - ) - ) - - const openSubView = useRef('cur_file') - useEffect(() => { - if (!reviewPanelOpen) { - // Always show current file when not open, but save current state - setSubView(prevState => { - openSubView.current = prevState - return 'cur_file' - }) - } else { - // Reset back to what we had when previously open - setSubView(openSubView.current) - } - handleLayoutChange({ async: true, animate: false }) - }, [reviewPanelOpen]) - - const canRefreshRanges = useRef(false) - const prevSubView = useRef(subView) - const initializedPrevSubView = useRef(false) - useEffect(() => { - // Prevent setting a computed value for `prevSubView` on mount - if (!initializedPrevSubView.current) { - initializedPrevSubView.current = true - return - } - prevSubView.current = subView === 'cur_file' ? 'overview' : 'cur_file' - // Allow refreshing ranges once for each `subView` change - canRefreshRanges.current = true - }, [subView]) - - useEffect(() => { - if (subView === 'overview' && canRefreshRanges.current) { - canRefreshRanges.current = false - - setIsOverviewLoading(true) - refreshRanges().finally(() => { - setIsOverviewLoading(false) - }) - } - }, [subView, refreshRanges]) - - useEffect(() => { - if (subView === 'cur_file' && prevSubView.current === 'overview') { - dispatchReviewPanelEvent('overview-closed', subView) - } - }, [subView]) - - useEffect(() => { - if (Object.keys(users).length) { - handleLayoutChange({ async: true }) - } - }, [users]) - - const values = useMemo( - () => ({ - collapsed, - commentThreads, - entries, - isAddingComment, - loadingThreads, - nVisibleSelectedChanges, - permissions, - users, - resolvedComments, - shouldCollapse, - navHeight, - toolbarHeight, - subView, - wantTrackChanges, - isOverviewLoading, - openDocId: currentDocumentId, - lineHeight, - trackChangesState, - trackChangesOnForEveryone, - trackChangesOnForGuests, - trackChangesForGuestsAvailable, - formattedProjectMembers, - layoutSuspended, - unsavedComment, - layoutToLeft, - }), - [ - collapsed, - commentThreads, - entries, - isAddingComment, - loadingThreads, - nVisibleSelectedChanges, - permissions, - users, - resolvedComments, - shouldCollapse, - navHeight, - toolbarHeight, - subView, - wantTrackChanges, - isOverviewLoading, - currentDocumentId, - lineHeight, - trackChangesState, - trackChangesOnForEveryone, - trackChangesOnForGuests, - trackChangesForGuestsAvailable, - formattedProjectMembers, - layoutSuspended, - unsavedComment, - layoutToLeft, - ] - ) - - const updaterFns = useMemo( - () => ({ - handleSetSubview, - handleLayoutChange, - gotoEntry, - resolveComment, - submitReply, - acceptChanges, - rejectChanges, - toggleReviewPanel, - bulkAcceptActions, - bulkRejectActions, - saveEdit, - submitNewComment, - deleteComment, - unresolveComment, - refreshResolvedCommentsDropdown: refreshRanges, - deleteThread, - toggleTrackChangesForEveryone, - toggleTrackChangesForUser, - toggleTrackChangesForGuests, - setCollapsed, - setShouldCollapse, - setIsAddingComment, - setNavHeight, - setToolbarHeight, - setLayoutSuspended, - setUnsavedComment, - }), - [ - handleSetSubview, - gotoEntry, - resolveComment, - submitReply, - acceptChanges, - rejectChanges, - toggleReviewPanel, - bulkAcceptActions, - bulkRejectActions, - saveEdit, - submitNewComment, - deleteComment, - unresolveComment, - refreshRanges, - deleteThread, - toggleTrackChangesForEveryone, - toggleTrackChangesForUser, - toggleTrackChangesForGuests, - setCollapsed, - setShouldCollapse, - setIsAddingComment, - setNavHeight, - setToolbarHeight, - setLayoutSuspended, - setUnsavedComment, - ] - ) - - return { values, updaterFns } -} - -export default useReviewPanelState diff --git a/services/web/modules/track-changes/app/src/TrackChangesController.js b/services/web/modules/track-changes/app/src/TrackChangesController.js index 12cbb57da4..45f8a03e0f 100644 --- a/services/web/modules/track-changes/app/src/TrackChangesController.js +++ b/services/web/modules/track-changes/app/src/TrackChangesController.js @@ -3,6 +3,7 @@ const ChatManager = require('../../../../app/src/Features/Chat/ChatManager') const EditorRealTimeController = require('../../../../app/src/Features/Editor/EditorRealTimeController') const SessionManager = require('../../../../app/src/Features/Authentication/SessionManager') const UserInfoManager = require('../../../../app/src/Features/User/UserInfoManager') +const UserInfoController = require('../../../../app/src/Features/User/UserInfoController') const DocstoreManager = require('../../../../app/src/Features/Docstore/DocstoreManager') const DocumentUpdaterHandler = require('../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler') const CollaboratorsGetter = require('../../../../app/src/Features/Collaborators/CollaboratorsGetter') @@ -11,10 +12,10 @@ const pLimit = require('p-limit') function _transformId(doc) { if (doc._id) { - doc.id = doc._id; - delete doc._id; + doc.id = doc._id + delete doc._id } - return doc; + return doc } const TrackChangesController = { @@ -44,7 +45,6 @@ const TrackChangesController = { async getAllRanges(req, res, next) { try { const { project_id } = req.params - // Flushing the project to mongo is not ideal. Is it possible to fetch the ranges from redis? await DocumentUpdaterHandler.promises.flushProjectToMongo(project_id) const ranges = await DocstoreManager.promises.getAllRanges(project_id) res.json(ranges.map(_transformId)) @@ -53,31 +53,12 @@ const TrackChangesController = { } }, async getChangesUsers(req, res, next) { +// This route was previously used by the frontend to retrieve names of users who made changes or comments. +// review-panel-new no longer needs this for comments, but still relies on it for changes - +// although the frontend knows the names of the current owner and members, it depends on the data +// provided here to assign names to authors who have left the project but still have unaccepted changes. try { const { project_id } = req.params - const memberIds = await CollaboratorsGetter.promises.getMemberIds(project_id) - // FIXME: Fails to display names in changes made by former project collaborators. - // See the alternative below. However, it requires flushing the project to mongo, which is not ideal. - const limit = pLimit(3) - const users = await Promise.all( - memberIds.map(memberId => - limit(async () => { - const user = await UserInfoManager.promises.getPersonalInfo(memberId) - return user - }) - ) - ) - users.push({_id: null}) // An anonymous user won't cause any harm - res.json(users.map(_transformId)) - } catch (err) { - next(err) - } - }, -/* - async getChangesUsers(req, res, next) { - try { - const { project_id } = req.params - await DocumentUpdaterHandler.promises.flushProjectToMongo(project_id) const memberIds = new Set() const ranges = await DocstoreManager.promises.getAllRanges(project_id) ranges.forEach(range => { @@ -89,20 +70,16 @@ const TrackChangesController = { const users = await Promise.all( [...memberIds].map(memberId => limit(async () => { - if( memberId !== "anonymous-user") { - return await UserInfoManager.promises.getPersonalInfo(memberId) - } else { - return {_id: null} - } + const user = await UserInfoManager.promises.getPersonalInfo(memberId) + return UserInfoController.formatPersonalInfo(user) }) ) ) - res.json(users.map(_transformId)) + res.json(users) } catch (err) { next(err) } }, -*/ async getThreads(req, res, next) { try { const { project_id } = req.params @@ -120,11 +97,12 @@ const TrackChangesController = { const user_id = SessionManager.getLoggedInUserId(req.session) if (!user_id) throw new Error('no logged-in user') const message = await ChatApiHandler.promises.sendComment(project_id, thread_id, user_id, content) - message.user = await UserInfoManager.promises.getPersonalInfo(user_id) + const user = await UserInfoManager.promises.getPersonalInfo(user_id) + message.user = UserInfoController.formatPersonalInfo(user) EditorRealTimeController.emitToRoom(project_id, 'new-comment', thread_id, message) res.sendStatus(204) } catch (err) { - next(err); + next(err) } }, async editMessage(req, res, next) { @@ -157,11 +135,16 @@ const TrackChangesController = { if (!user_id) throw new Error('no logged-in user') const user = await UserInfoManager.promises.getPersonalInfo(user_id) await ChatApiHandler.promises.resolveThread(project_id, thread_id, user_id) - EditorRealTimeController.emitToRoom(project_id, 'resolve-thread', thread_id, user) + EditorRealTimeController.emitToRoom( + project_id, + 'resolve-thread', + thread_id, + UserInfoController.formatPersonalInfo(user) + ) await DocumentUpdaterHandler.promises.resolveThread(project_id, doc_id, thread_id, user_id) - res.sendStatus(204); + res.sendStatus(204) } catch (err) { - next(err); + next(err) } }, async reopenThread(req, res, next) {