mirror of
https://github.com/overleaf/overleaf.git
synced 2024-12-03 09:44:03 -05:00
292 lines
7.7 KiB
TypeScript
292 lines
7.7 KiB
TypeScript
|
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
|
||
|
}
|