overleaf/services/web/frontend/js/features/review-panel-new/context/threads-context.tsx

292 lines
7.7 KiB
TypeScript
Raw Normal View History

Add review panel context providers and components (#19490) * Tidy up review panel components * Add ReviewPanel providers * [web] new design for review panel track change (#19544) * [web] new design for review panel track change * fixed mini view * mini icon style change * fix icon size * format date * useRangesUserContext hook * remove useRangesUserContext hook * using full class names * fix action icons hover * change wording for tooltips * added ReviewPanelChangeUser component * Update header in new review panel * Extract ReviewPanelTrackChangesMenuButton as a separate component * Remove wrapper div * Replace h2 with div for review panel label * Rename ReviewPanelTools to ReviewPanelHeader * Rename trackChangesExpanded -> trackChangesMenuExpanded * Dont break memoisation of ReviewPanelTrackChangesMenuButton * Fix the width of the track changes arrow icon * Update how prop types are declared * Remove new empty state from old review panel * Add empty state to new review panel * Add project members and owner to ChangesUsers context (#19624) --------- Co-authored-by: Alf Eaton <alf.eaton@overleaf.com> * Redesign comment entry in review panel (#19678) * Redesign comment entry in review panel * ReviewPanelCommentOptions component * remove unused prop * Tidying * Add conditional import * Optional changeManager * Add more split test compatibility * More split test compatibility * Fixes * Improve overview scrolling * Fix overview scrolling * Fix & simplify track changes toggle * Fix overview scrolling * Fix current file container * ExpandableContent component for messages in review panel (#19738) * ExpandableContent component for messages in review panel * remove isExpanded dependancy * Delete comment option for new review panel (#19772) * Delete comment option for new review panel * dont show thread warning if there are no replies * fix hasReplies issue * Implement initial collapsing overview files * Fix positioning of overview panel * Small styling changes * Add count of unresolved comments and tracked chanegs * More style adjustments * Move review-panel-overview styles into css file * Remove unused var --------- Co-authored-by: Domagoj Kriskovic <dom.kriskovic@overleaf.com> Co-authored-by: David Powell <david.powell@overleaf.com> Co-authored-by: David <33458145+davidmcpowell@users.noreply.github.com> GitOrigin-RevId: e67463443d541f88445a86eed5e2b6ec6040f9c7
2024-08-12 05:50:54 -04:00
import {
createContext,
FC,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react'
import { useProjectContext } from '@/shared/context/project-context'
import {
CommentId,
ThreadId,
} from '../../../../../types/review-panel/review-panel'
import { ReviewPanelCommentThread } from '../../../../../types/review-panel/comment-thread'
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
import { ReviewPanelCommentThreadMessageApi } from '../../../../../types/review-panel/api'
import { UserId } from '../../../../../types/user'
import { deleteJSON, getJSON, postJSON } from '@/infrastructure/fetch-json'
import RangesTracker from '@overleaf/ranges-tracker'
import { CommentOperation } from '../../../../../types/change'
import useScopeValue from '@/shared/hooks/use-scope-value'
import { DocumentContainer } from '@/features/ide-react/editor/document-container'
export type Threads = Record<ThreadId, ReviewPanelCommentThread>
export const ThreadsContext = createContext<Threads | undefined>(undefined)
type ThreadsActions = {
addComment: (pos: number, text: string, content: string) => Promise<void>
resolveThread: (threadId: ThreadId) => Promise<void>
reopenThread: (threadId: ThreadId) => Promise<void>
deleteThread: (threadId: ThreadId) => Promise<void>
addMessage: (threadId: ThreadId, content: string) => Promise<void>
editMessage: (
threadId: ThreadId,
commentId: CommentId,
content: string
) => Promise<void>
deleteMessage: (threadId: ThreadId, commentId: CommentId) => Promise<void>
}
const ThreadsActionsContext = createContext<ThreadsActions | undefined>(
undefined
)
export const ThreadsProvider: FC = ({ children }) => {
const { _id: projectId } = useProjectContext()
const [currentDoc] = useScopeValue<DocumentContainer | null>(
'editor.sharejs_doc'
)
// const [error, setError] = useState<Error>()
const [data, setData] = useState<Threads>()
// load the initial threads data
useEffect(() => {
const abortController = new AbortController()
getJSON(`/project/${projectId}/threads`, {
signal: abortController.signal,
}).then(data => {
setData(data)
})
// .catch(error => {
// setError(error)
// })
}, [projectId])
const { socket } = useConnectionContext()
useSocketListener(
socket,
'new-comment',
useCallback(
(threadId: ThreadId, comment: ReviewPanelCommentThreadMessageApi) => {
setData(value => {
if (value) {
const { submitting, ...thread } = value[threadId] ?? {
messages: [],
}
return {
...value,
[threadId]: {
...thread,
messages: [
...thread.messages,
{
...comment,
user: comment.user, // TODO
timestamp: new Date(comment.timestamp),
},
],
},
}
}
})
},
[]
)
)
useSocketListener(
socket,
'edit-message',
useCallback((threadId: ThreadId, commentId: CommentId, content: string) => {
setData(value => {
if (value) {
const thread = value[threadId] ?? { messages: [] }
return {
...value,
[threadId]: {
...thread,
messages: thread.messages.map(message =>
message.id === commentId ? { ...message, content } : message
),
},
}
}
})
}, [])
)
useSocketListener(
socket,
'delete-message',
useCallback((threadId: ThreadId, commentId: CommentId) => {
setData(value => {
if (value) {
const thread = value[threadId] ?? { messages: [] }
return {
...value,
[threadId]: {
...thread,
messages: thread.messages.filter(
message => message.id !== commentId
),
},
}
}
})
}, [])
)
useSocketListener(
socket,
'resolve-thread',
useCallback(
(
threadId: ThreadId,
user: { email: string; first_name: string; id: UserId }
) => {
setData(value => {
if (value) {
const thread = value[threadId] ?? { messages: [] }
return {
...value,
[threadId]: {
...thread,
resolved: true,
resolved_by_user: user, // TODO
resolved_at: new Date().toISOString(),
},
}
}
})
},
[]
)
)
useSocketListener(
socket,
'reopen-thread',
useCallback((threadId: ThreadId) => {
setData(value => {
if (value) {
const thread = value[threadId] ?? { messages: [] }
return {
...value,
[threadId]: {
...thread,
resolved: undefined,
resolved_by_user: undefined,
resolved_at: undefined,
},
}
}
})
}, [])
)
useSocketListener(
socket,
'delete-thread',
useCallback((threadId: ThreadId) => {
setData(value => {
if (value) {
const _value = { ...value }
delete _value[threadId]
return _value
}
})
}, [])
)
const actions = useMemo(
() => ({
async addComment(pos: number, text: string, content: string) {
const threadId = RangesTracker.generateId() as ThreadId
await postJSON(`/project/${projectId}/thread/${threadId}/messages`, {
body: { content },
})
const op: CommentOperation = {
c: text,
p: pos,
t: threadId,
}
currentDoc?.submitOp(op)
},
async resolveThread(threadId: string) {
await postJSON(
`/project/${projectId}/doc/${currentDoc?.doc_id}/thread/${threadId}/resolve`
)
},
async reopenThread(threadId: string) {
await postJSON(
`/project/${projectId}/doc/${currentDoc?.doc_id}/thread/${threadId}/reopen`
)
},
async deleteThread(threadId: string) {
await deleteJSON(
`/project/${projectId}/doc/${currentDoc?.doc_id}/thread/${threadId}`
)
currentDoc?.ranges?.removeCommentId(threadId)
},
async addMessage(threadId: ThreadId, content: string) {
await postJSON(`/project/${projectId}/thread/${threadId}/messages`, {
body: { content },
})
// TODO: error_submitting_comment
},
async editMessage(
threadId: ThreadId,
commentId: CommentId,
content: string
) {
await postJSON(
`/project/${projectId}/thread/${threadId}/messages/${commentId}/edit`,
{ body: { content } }
)
},
async deleteMessage(threadId: ThreadId, commentId: CommentId) {
await deleteJSON(
`/project/${projectId}/thread/${threadId}/messages/${commentId}`
)
},
}),
[currentDoc, projectId]
)
return (
<ThreadsActionsContext.Provider value={actions}>
<ThreadsContext.Provider value={data}>{children}</ThreadsContext.Provider>
</ThreadsActionsContext.Provider>
)
}
export const useThreadsContext = () => {
return useContext(ThreadsContext)
}
export const useThreadsActionsContext = () => {
const context = useContext(ThreadsActionsContext)
if (!context) {
throw new Error(
'useThreadsActionsContext is only available inside ThreadsProvider'
)
}
return context
}