first commit
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
createContext,
|
||||
FC,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { getJSON } from '@/infrastructure/fetch-json'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { UserId } from '../../../../../types/user'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { captureException } from '@/infrastructure/error-reporter'
|
||||
|
||||
export type ChangesUser = {
|
||||
id: UserId
|
||||
email: string
|
||||
first_name?: string
|
||||
last_name?: string
|
||||
}
|
||||
|
||||
export type ChangesUsers = Map<UserId, ChangesUser>
|
||||
|
||||
export const ChangesUsersContext = createContext<ChangesUsers | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
export const ChangesUsersProvider: FC = ({ children }) => {
|
||||
const { _id: projectId, members, owner } = useProjectContext()
|
||||
const { isRestrictedTokenMember } = useEditorContext()
|
||||
|
||||
const [changesUsers, setChangesUsers] = useState<ChangesUsers>()
|
||||
|
||||
useEffect(() => {
|
||||
if (isRestrictedTokenMember) {
|
||||
return
|
||||
}
|
||||
|
||||
getJSON<ChangesUser[]>(`/project/${projectId}/changes/users`)
|
||||
.then(data => setChangesUsers(new Map(data.map(item => [item.id, item]))))
|
||||
.catch(error => {
|
||||
debugConsole.error(error)
|
||||
captureException(error)
|
||||
})
|
||||
}, [projectId, isRestrictedTokenMember])
|
||||
|
||||
// add the project owner and members to the changes users data
|
||||
const value = useMemo(() => {
|
||||
const value: ChangesUsers = new Map(changesUsers)
|
||||
value.set(owner._id, { ...owner, id: owner._id })
|
||||
for (const member of members) {
|
||||
value.set(member._id, { ...member, id: member._id })
|
||||
}
|
||||
return value
|
||||
}, [members, owner, changesUsers])
|
||||
|
||||
return (
|
||||
<ChangesUsersContext.Provider value={value}>
|
||||
{children}
|
||||
</ChangesUsersContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useChangesUsersContext = () => {
|
||||
return useContext(ChangesUsersContext)
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import {
|
||||
createContext,
|
||||
FC,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { DocumentContainer } from '@/features/ide-react/editor/document-container'
|
||||
import {
|
||||
Change,
|
||||
CommentOperation,
|
||||
EditOperation,
|
||||
} from '../../../../../types/change'
|
||||
import RangesTracker from '@overleaf/ranges-tracker'
|
||||
import { rejectChanges } from '@/features/source-editor/extensions/changes/reject-changes'
|
||||
import { useCodeMirrorViewContext } from '@/features/source-editor/components/codemirror-context'
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
||||
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||
import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
|
||||
import { throttle } from 'lodash'
|
||||
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
||||
|
||||
export type Ranges = {
|
||||
docId: string
|
||||
changes: Change<EditOperation>[]
|
||||
comments: Change<CommentOperation>[]
|
||||
}
|
||||
|
||||
export const RangesContext = createContext<Ranges | undefined>(undefined)
|
||||
|
||||
type RangesActions = {
|
||||
acceptChanges: (...ids: string[]) => void
|
||||
rejectChanges: (...ids: string[]) => void
|
||||
}
|
||||
|
||||
const buildRanges = (currentDocument: DocumentContainer | null) => {
|
||||
const ranges = currentDocument?.ranges
|
||||
|
||||
if (!ranges) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const dirtyState = ranges.getDirtyState()
|
||||
ranges.resetDirtyState()
|
||||
|
||||
const changed = {
|
||||
changes: new Set([
|
||||
...Object.keys(dirtyState.change.added),
|
||||
...Object.keys(dirtyState.change.moved),
|
||||
...Object.keys(dirtyState.change.removed),
|
||||
]),
|
||||
comments: new Set([
|
||||
...Object.keys(dirtyState.comment.added),
|
||||
...Object.keys(dirtyState.comment.moved),
|
||||
...Object.keys(dirtyState.comment.removed),
|
||||
]),
|
||||
}
|
||||
|
||||
return {
|
||||
changes:
|
||||
changed.changes.size > 0
|
||||
? ranges.changes.map(change =>
|
||||
changed.changes.has(change.id) ? { ...change } : change
|
||||
)
|
||||
: ranges.changes,
|
||||
comments:
|
||||
changed.comments.size > 0
|
||||
? ranges.comments.map(comment =>
|
||||
changed.comments.has(comment.id) ? { ...comment } : comment
|
||||
)
|
||||
: ranges.comments,
|
||||
docId: currentDocument.doc_id,
|
||||
}
|
||||
}
|
||||
|
||||
const RangesActionsContext = createContext<RangesActions | undefined>(undefined)
|
||||
|
||||
export const RangesProvider: FC = ({ children }) => {
|
||||
const view = useCodeMirrorViewContext()
|
||||
const { projectId } = useIdeReactContext()
|
||||
const { currentDocument } = useEditorManagerContext()
|
||||
const { socket } = useConnectionContext()
|
||||
const [ranges, setRanges] = useState<Ranges | undefined>(() =>
|
||||
buildRanges(currentDocument)
|
||||
)
|
||||
|
||||
// rebuild the ranges when the current doc changes
|
||||
useEffect(() => {
|
||||
setRanges(buildRanges(currentDocument))
|
||||
}, [currentDocument])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDocument) {
|
||||
const listener = throttle(
|
||||
() => {
|
||||
window.setTimeout(() => {
|
||||
setRanges(buildRanges(currentDocument))
|
||||
})
|
||||
},
|
||||
500,
|
||||
{ leading: true, trailing: true }
|
||||
)
|
||||
|
||||
// currentDocument.on('ranges:clear.cm6', listener)
|
||||
currentDocument.on('ranges:redraw.cm6', listener)
|
||||
currentDocument.on('ranges:dirty.cm6', listener)
|
||||
|
||||
return () => {
|
||||
// currentDocument.off('ranges:clear.cm6')
|
||||
currentDocument.off('ranges:redraw.cm6')
|
||||
currentDocument.off('ranges:dirty.cm6')
|
||||
}
|
||||
}
|
||||
}, [currentDocument])
|
||||
|
||||
// TODO: move this into DocumentContainer?
|
||||
useEffect(() => {
|
||||
if (currentDocument) {
|
||||
const regenerateTrackChangesId = (doc: DocumentContainer) => {
|
||||
if (doc.ranges) {
|
||||
const inflight = doc.ranges.getIdSeed()
|
||||
const pending = RangesTracker.generateIdSeed()
|
||||
doc.ranges.setIdSeed(pending)
|
||||
doc.setTrackChangesIdSeeds({ pending, inflight })
|
||||
}
|
||||
}
|
||||
|
||||
currentDocument.on('flipped_pending_to_inflight', () =>
|
||||
regenerateTrackChangesId(currentDocument)
|
||||
)
|
||||
|
||||
regenerateTrackChangesId(currentDocument)
|
||||
|
||||
return () => {
|
||||
currentDocument.off('flipped_pending_to_inflight')
|
||||
}
|
||||
}
|
||||
}, [currentDocument])
|
||||
|
||||
useSocketListener(
|
||||
socket,
|
||||
'accept-changes',
|
||||
useCallback(
|
||||
(docId: string, entryIds: string[]) => {
|
||||
if (currentDocument?.ranges) {
|
||||
if (docId === currentDocument.doc_id) {
|
||||
currentDocument.ranges.removeChangeIds(entryIds)
|
||||
setRanges(buildRanges(currentDocument))
|
||||
}
|
||||
}
|
||||
},
|
||||
[currentDocument]
|
||||
)
|
||||
)
|
||||
|
||||
const actions = useMemo(
|
||||
() => ({
|
||||
async acceptChanges(...ids: string[]) {
|
||||
if (currentDocument?.ranges) {
|
||||
const url = `/project/${projectId}/doc/${currentDocument.doc_id}/changes/accept`
|
||||
await postJSON(url, { body: { change_ids: ids } })
|
||||
currentDocument.ranges.removeChangeIds(ids)
|
||||
setRanges(buildRanges(currentDocument))
|
||||
}
|
||||
},
|
||||
rejectChanges(...ids: string[]) {
|
||||
if (currentDocument?.ranges) {
|
||||
view.dispatch(rejectChanges(view.state, currentDocument.ranges, ids))
|
||||
}
|
||||
},
|
||||
}),
|
||||
[currentDocument, projectId, view]
|
||||
)
|
||||
|
||||
return (
|
||||
<RangesActionsContext.Provider value={actions}>
|
||||
<RangesContext.Provider value={ranges}>{children}</RangesContext.Provider>
|
||||
</RangesActionsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useRangesContext = () => {
|
||||
return useContext(RangesContext)
|
||||
}
|
||||
|
||||
export const useRangesActionsContext = () => {
|
||||
const context = useContext(RangesActionsContext)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useRangesActionsContext is only available inside RangesProvider'
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { FC } from 'react'
|
||||
import { RangesProvider } from './ranges-context'
|
||||
import { ChangesUsersProvider } from './changes-users-context'
|
||||
import { TrackChangesStateProvider } from './track-changes-state-context'
|
||||
import { ThreadsProvider } from './threads-context'
|
||||
import { ReviewPanelViewProvider } from './review-panel-view-context'
|
||||
|
||||
export const ReviewPanelProviders: FC = ({ children }) => {
|
||||
return (
|
||||
<ReviewPanelViewProvider>
|
||||
<ChangesUsersProvider>
|
||||
<TrackChangesStateProvider>
|
||||
<ThreadsProvider>
|
||||
<RangesProvider>{children}</RangesProvider>
|
||||
</ThreadsProvider>
|
||||
</TrackChangesStateProvider>
|
||||
</ChangesUsersProvider>
|
||||
</ReviewPanelViewProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
FC,
|
||||
SetStateAction,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
export type View = 'cur_file' | 'overview'
|
||||
|
||||
export const ReviewPanelViewContext = createContext<View>('cur_file')
|
||||
|
||||
type ViewActions = {
|
||||
setView: Dispatch<SetStateAction<View>>
|
||||
}
|
||||
|
||||
const ReviewPanelViewActionsContext = createContext<ViewActions | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
export const ReviewPanelViewProvider: FC = ({ children }) => {
|
||||
const [view, setView] = useState<View>('cur_file')
|
||||
|
||||
const actions = useMemo(
|
||||
() => ({
|
||||
setView,
|
||||
}),
|
||||
[setView]
|
||||
)
|
||||
|
||||
return (
|
||||
<ReviewPanelViewActionsContext.Provider value={actions}>
|
||||
<ReviewPanelViewContext.Provider value={view}>
|
||||
{children}
|
||||
</ReviewPanelViewContext.Provider>
|
||||
</ReviewPanelViewActionsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useReviewPanelViewContext = () => {
|
||||
return useContext(ReviewPanelViewContext)
|
||||
}
|
||||
|
||||
export const useReviewPanelViewActionsContext = () => {
|
||||
const context = useContext(ReviewPanelViewActionsContext)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useViewActionsContext is only available inside ViewProvider'
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
import {
|
||||
createContext,
|
||||
FC,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import {
|
||||
CommentId,
|
||||
ReviewPanelCommentThreadMessage,
|
||||
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 { UserId } from '../../../../../types/user'
|
||||
import { deleteJSON, getJSON, postJSON } from '@/infrastructure/fetch-json'
|
||||
import RangesTracker from '@overleaf/ranges-tracker'
|
||||
import { CommentOperation } from '../../../../../types/change'
|
||||
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { captureException } from '@/infrastructure/error-reporter'
|
||||
|
||||
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>
|
||||
deleteOwnMessage: (threadId: ThreadId, commentId: CommentId) => Promise<void>
|
||||
}
|
||||
|
||||
const ThreadsActionsContext = createContext<ThreadsActions | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
export const ThreadsProvider: FC = ({ children }) => {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { currentDocument } = useEditorManagerContext()
|
||||
const { isRestrictedTokenMember } = useEditorContext()
|
||||
|
||||
// const [error, setError] = useState<Error>()
|
||||
const [data, setData] = useState<Threads>()
|
||||
|
||||
// load the initial threads data
|
||||
useEffect(() => {
|
||||
if (isRestrictedTokenMember) {
|
||||
return
|
||||
}
|
||||
|
||||
const abortController = new AbortController()
|
||||
|
||||
getJSON(`/project/${projectId}/threads`, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
.then(data => {
|
||||
setData(data)
|
||||
})
|
||||
.catch(error => {
|
||||
debugConsole.error(error)
|
||||
captureException(error)
|
||||
// setError(error)
|
||||
})
|
||||
}, [projectId, isRestrictedTokenMember])
|
||||
|
||||
const { socket } = useConnectionContext()
|
||||
|
||||
useSocketListener(
|
||||
socket,
|
||||
'new-comment',
|
||||
useCallback(
|
||||
(
|
||||
threadId: ThreadId,
|
||||
comment: ReviewPanelCommentThreadMessage & { timestamp: number }
|
||||
) => {
|
||||
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
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
)
|
||||
|
||||
useSocketListener(
|
||||
socket,
|
||||
'new-comment-threads',
|
||||
useCallback(threads => {
|
||||
setData(prevState => {
|
||||
const newThreads = { ...prevState }
|
||||
for (const threadId of Object.keys(threads)) {
|
||||
const thread = threads[threadId]
|
||||
const newThreadData: ReviewPanelCommentThread = {
|
||||
messages: [],
|
||||
resolved: thread.resolved,
|
||||
resolved_at: thread.resolved_at,
|
||||
resolved_by_user_id: thread.resolved_by_user_id,
|
||||
resolved_by_user: thread.resolved_by_user,
|
||||
}
|
||||
for (const message of thread.messages) {
|
||||
newThreadData.messages.push({
|
||||
...message,
|
||||
timestamp: new Date(message.timestamp),
|
||||
})
|
||||
}
|
||||
newThreads[threadId as ThreadId] = newThreadData
|
||||
}
|
||||
return newThreads
|
||||
})
|
||||
}, [])
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
currentDocument?.submitOp(op)
|
||||
},
|
||||
async resolveThread(threadId: string) {
|
||||
await postJSON(
|
||||
`/project/${projectId}/doc/${currentDocument?.doc_id}/thread/${threadId}/resolve`
|
||||
)
|
||||
},
|
||||
async reopenThread(threadId: string) {
|
||||
await postJSON(
|
||||
`/project/${projectId}/doc/${currentDocument?.doc_id}/thread/${threadId}/reopen`
|
||||
)
|
||||
},
|
||||
async deleteThread(threadId: string) {
|
||||
await deleteJSON(
|
||||
`/project/${projectId}/doc/${currentDocument?.doc_id}/thread/${threadId}`
|
||||
)
|
||||
currentDocument?.ranges?.removeCommentId(threadId)
|
||||
},
|
||||
async addMessage(threadId: ThreadId, content: string) {
|
||||
await postJSON(`/project/${projectId}/thread/${threadId}/messages`, {
|
||||
body: { content },
|
||||
})
|
||||
},
|
||||
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}`
|
||||
)
|
||||
},
|
||||
async deleteOwnMessage(threadId: ThreadId, commentId: CommentId) {
|
||||
await deleteJSON(
|
||||
`/project/${projectId}/thread/${threadId}/own-messages/${commentId}`
|
||||
)
|
||||
},
|
||||
}),
|
||||
[currentDocument, 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
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { UserId } from '../../../../../types/user'
|
||||
import {
|
||||
createContext,
|
||||
FC,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
|
||||
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
||||
import { useUserContext } from '@/shared/context/user-context'
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import { ProjectContextValue } from '@/shared/context/types/project-context'
|
||||
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
export type TrackChangesState = {
|
||||
onForEveryone: boolean
|
||||
onForGuests: boolean
|
||||
onForMembers: Record<UserId, boolean | undefined>
|
||||
}
|
||||
|
||||
export const TrackChangesStateContext = createContext<
|
||||
TrackChangesState | undefined
|
||||
>(undefined)
|
||||
|
||||
type SaveTrackChangesRequestBody = {
|
||||
on?: boolean
|
||||
on_for?: Record<UserId, boolean | undefined>
|
||||
on_for_guests?: boolean
|
||||
}
|
||||
|
||||
type TrackChangesStateActions = {
|
||||
saveTrackChanges: (trackChangesBody: SaveTrackChangesRequestBody) => void
|
||||
saveTrackChangesForCurrentUser: (trackChanges: boolean) => void
|
||||
}
|
||||
|
||||
const TrackChangesStateActionsContext = createContext<
|
||||
TrackChangesStateActions | undefined
|
||||
>(undefined)
|
||||
|
||||
export const TrackChangesStateProvider: FC = ({ children }) => {
|
||||
const permissions = usePermissionsContext()
|
||||
const { socket } = useConnectionContext()
|
||||
const project = useProjectContext()
|
||||
const user = useUserContext()
|
||||
const { setWantTrackChanges } = useEditorManagerContext()
|
||||
|
||||
// TODO: update project.trackChangesState instead?
|
||||
const [trackChangesValue, setTrackChangesValue] = useState<
|
||||
ProjectContextValue['trackChangesState']
|
||||
>(project.trackChangesState ?? false)
|
||||
|
||||
useSocketListener(socket, 'toggle-track-changes', setTrackChangesValue)
|
||||
|
||||
useEffect(() => {
|
||||
setWantTrackChanges(
|
||||
trackChangesValue === true ||
|
||||
(trackChangesValue !== false &&
|
||||
trackChangesValue[user.id ?? '__guests__'])
|
||||
)
|
||||
}, [setWantTrackChanges, trackChangesValue, user.id])
|
||||
|
||||
const trackChangesIsObject =
|
||||
trackChangesValue !== true && trackChangesValue !== false
|
||||
const onForEveryone = trackChangesValue === true
|
||||
const onForGuests =
|
||||
onForEveryone ||
|
||||
(trackChangesIsObject && trackChangesValue.__guests__ === true)
|
||||
|
||||
const onForMembers = useMemo(() => {
|
||||
const onForMembers: Record<UserId, boolean | undefined> = {}
|
||||
if (trackChangesIsObject) {
|
||||
for (const key of Object.keys(trackChangesValue)) {
|
||||
if (key !== '__guests__') {
|
||||
onForMembers[key as UserId] = trackChangesValue[key as UserId]
|
||||
}
|
||||
}
|
||||
}
|
||||
return onForMembers
|
||||
}, [trackChangesIsObject, trackChangesValue])
|
||||
|
||||
const saveTrackChanges = useCallback(
|
||||
async (trackChangesBody: SaveTrackChangesRequestBody) => {
|
||||
postJSON(`/project/${project._id}/track_changes`, {
|
||||
body: trackChangesBody,
|
||||
})
|
||||
},
|
||||
[project._id]
|
||||
)
|
||||
|
||||
const saveTrackChangesForCurrentUser = useCallback(
|
||||
async (trackChanges: boolean) => {
|
||||
if (user.id) {
|
||||
if (getMeta('ol-isReviewerRoleEnabled')) {
|
||||
saveTrackChanges({
|
||||
on_for: {
|
||||
...onForMembers,
|
||||
[user.id]: trackChanges,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
saveTrackChanges({
|
||||
on_for: {
|
||||
...onForMembers,
|
||||
[user.id]: trackChanges,
|
||||
},
|
||||
on_for_guests: onForGuests,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
[onForMembers, onForGuests, user.id, saveTrackChanges]
|
||||
)
|
||||
|
||||
const actions = useMemo(
|
||||
() => ({
|
||||
saveTrackChanges,
|
||||
saveTrackChangesForCurrentUser,
|
||||
}),
|
||||
[saveTrackChanges, saveTrackChangesForCurrentUser]
|
||||
)
|
||||
|
||||
useEventListener(
|
||||
'toggle-track-changes',
|
||||
useCallback(() => {
|
||||
if (
|
||||
user.id &&
|
||||
project.features.trackChanges &&
|
||||
permissions.write &&
|
||||
!onForEveryone
|
||||
) {
|
||||
const value = onForMembers[user.id]
|
||||
if (getMeta('ol-isReviewerRoleEnabled')) {
|
||||
actions.saveTrackChanges({
|
||||
on_for: {
|
||||
...onForMembers,
|
||||
[user.id]: !value,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
actions.saveTrackChanges({
|
||||
on_for: {
|
||||
...onForMembers,
|
||||
[user.id]: !value,
|
||||
},
|
||||
on_for_guests: onForGuests,
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [
|
||||
actions,
|
||||
onForMembers,
|
||||
onForGuests,
|
||||
onForEveryone,
|
||||
permissions.write,
|
||||
project.features.trackChanges,
|
||||
user.id,
|
||||
])
|
||||
)
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ onForEveryone, onForGuests, onForMembers }),
|
||||
[onForEveryone, onForGuests, onForMembers]
|
||||
)
|
||||
|
||||
return (
|
||||
<TrackChangesStateActionsContext.Provider value={actions}>
|
||||
<TrackChangesStateContext.Provider value={value}>
|
||||
{children}
|
||||
</TrackChangesStateContext.Provider>
|
||||
</TrackChangesStateActionsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useTrackChangesStateContext = () => {
|
||||
return useContext(TrackChangesStateContext)
|
||||
}
|
||||
|
||||
export const useTrackChangesStateActionsContext = () => {
|
||||
const context = useContext(TrackChangesStateActionsContext)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useTrackChangesStateActionsContext is only available inside TrackChangesStateProvider'
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
Reference in New Issue
Block a user