| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- import {
- createContext,
- useContext,
- useEffect,
- FC,
- useCallback,
- useMemo,
- useState,
- useRef,
- } from 'react'
- import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
- import { useConnectionContext } from '@/features/ide-react/context/connection-context'
- import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
- import { getJSON, postJSON } from '@/infrastructure/fetch-json'
- import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
- import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
- import useEventListener from '@/shared/hooks/use-event-listener'
- import { useModalsContext } from '@/features/ide-react/context/modals-context'
- import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
- import { useTranslation } from 'react-i18next'
- import { IdeEvents } from '@/features/ide-react/create-ide-event-emitter'
- export type Command = {
- caption: string
- snippet: string
- meta: string
- score: number
- }
- export type DocumentMetadata = {
- labels: string[]
- packages: Record<string, Command[]>
- packageNames: string[]
- }
- type DocumentsMetadata = Record<string, DocumentMetadata>
- type DocMetadataResponse = { docId: string; meta: DocumentMetadata }
- export const MetadataContext = createContext<
- | {
- commands: Command[]
- labels: Set<string>
- packageNames: Set<string>
- }
- | undefined
- >(undefined)
- export const MetadataProvider: FC<React.PropsWithChildren> = ({ children }) => {
- const { t } = useTranslation()
- const { eventEmitter, permissionsLevel, projectId } = useIdeReactContext()
- const { socket } = useConnectionContext()
- const { onlineUsersCount } = useOnlineUsersContext()
- const permissions = usePermissionsContext()
- const { currentDocument } = useEditorOpenDocContext()
- const { showGenericMessageModal } = useModalsContext()
- const [documents, setDocuments] = useState<DocumentsMetadata>({})
- const debouncerRef = useRef<Map<string, number>>(new Map()) // DocId => Timeout
- useEffect(() => {
- const handleEntityDeleted = ({
- detail: [entity],
- }: CustomEvent<IdeEvents['entity:deleted']>) => {
- if (entity.type === 'doc') {
- setDocuments(documents => {
- delete documents[entity.entity._id]
- return { ...documents }
- })
- }
- }
- eventEmitter.on('entity:deleted', handleEntityDeleted)
- return () => {
- eventEmitter.off('entity:deleted', handleEntityDeleted)
- }
- }, [eventEmitter])
- useEffect(() => {
- window.dispatchEvent(
- new CustomEvent('project:metadata', { detail: documents })
- )
- }, [documents])
- const onBroadcastDocMeta = useCallback((data: DocMetadataResponse) => {
- const { docId, meta } = data
- if (docId != null && meta != null) {
- setDocuments(documents => ({ ...documents, [docId]: meta }))
- }
- }, [])
- const loadProjectMetaFromServer = useCallback(() => {
- getJSON(`/project/${projectId}/metadata`).then(
- (response: { projectMeta: DocumentsMetadata }) => {
- const { projectMeta } = response
- if (projectMeta) {
- setDocuments(projectMeta)
- }
- }
- )
- }, [projectId])
- const loadDocMetaFromServer = useCallback(
- (docId: string) => {
- // Don't broadcast metadata when there are no other users in the
- // project.
- const broadcast = onlineUsersCount > 0
- postJSON(`/project/${projectId}/doc/${docId}/metadata`, {
- body: {
- broadcast,
- },
- }).then((response: DocMetadataResponse) => {
- if (!broadcast && response) {
- // handle the POST response like a broadcast event when there are no
- // other users in the project.
- onBroadcastDocMeta(response)
- }
- })
- },
- [onBroadcastDocMeta, onlineUsersCount, projectId]
- )
- const scheduleLoadDocMetaFromServer = useCallback(
- (docId: string) => {
- if (permissionsLevel === 'readOnly') {
- // The POST request is blocked for users without write permission.
- // The user will not be able to consume the metadata for edits anyway.
- return
- }
- // Debounce loading labels with a timeout
- const existingTimeout = debouncerRef.current.get(docId)
- if (existingTimeout != null) {
- window.clearTimeout(existingTimeout)
- debouncerRef.current.delete(docId)
- }
- debouncerRef.current.set(
- docId,
- window.setTimeout(() => {
- // TODO: wait for the document to be saved?
- loadDocMetaFromServer(docId)
- debouncerRef.current.delete(docId)
- }, 2000)
- )
- },
- [loadDocMetaFromServer, permissionsLevel]
- )
- const handleBroadcastDocMeta = useCallback(
- (data: DocMetadataResponse) => {
- onBroadcastDocMeta(data)
- },
- [onBroadcastDocMeta]
- )
- useSocketListener(socket, 'broadcastDocMeta', handleBroadcastDocMeta)
- const handleMetadataOutdated = useCallback(() => {
- if (currentDocument) {
- scheduleLoadDocMetaFromServer(currentDocument.doc_id)
- }
- }, [currentDocument, scheduleLoadDocMetaFromServer])
- useEventListener('editor:metadata-outdated', handleMetadataOutdated)
- const permissionsRef = useRef(permissions)
- useEffect(() => {
- permissionsRef.current = permissions
- }, [permissions])
- useEffect(() => {
- const handleProjectJoined = ({
- detail: [{ project }],
- }: CustomEvent<IdeEvents['project:joined']>) => {
- if (project.deletedByExternalDataSource) {
- showGenericMessageModal(
- t('project_renamed_or_deleted'),
- t('project_renamed_or_deleted_detail')
- )
- }
- window.setTimeout(() => {
- if (
- permissionsRef.current.write ||
- permissionsRef.current.trackedWrite
- ) {
- loadProjectMetaFromServer()
- }
- }, 200)
- }
- eventEmitter.once('project:joined', handleProjectJoined)
- return () => {
- eventEmitter.off('project:joined', handleProjectJoined)
- }
- }, [eventEmitter, loadProjectMetaFromServer, showGenericMessageModal, t])
- const value = useMemo(() => {
- const docs = Object.values(documents)
- return {
- commands: docs.flatMap(doc => Object.values(doc.packages).flat()),
- labels: new Set(docs.flatMap(doc => doc.labels)),
- packageNames: new Set(docs.flatMap(doc => doc.packageNames)),
- }
- }, [documents])
- return (
- <MetadataContext.Provider value={value}>
- {children}
- </MetadataContext.Provider>
- )
- }
- export function useMetadataContext() {
- const context = useContext(MetadataContext)
- if (!context) {
- throw new Error(
- 'useMetadataContext is only available inside MetadataProvider'
- )
- }
- return context
- }
|