overleaf/services/web/frontend/js/features/review-panel-new/context/threads-context.tsx
Alf Eaton 2304536844 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-13 08:04:20 +00:00

291 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
}