| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- import React, {
- createContext,
- useContext,
- useState,
- FC,
- useMemo,
- useEffect,
- useCallback,
- } from 'react'
- import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
- import { IdeProvider } from '@/shared/context/ide-context'
- import {
- createIdeEventEmitter,
- IdeEventEmitter,
- } from '@/features/ide-react/create-ide-event-emitter'
- import { JoinProjectPayload } from '@/features/ide-react/connection/join-project-payload'
- import { useConnectionContext } from '@/features/ide-react/context/connection-context'
- import { postJSON } from '@/infrastructure/fetch-json'
- import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
- import getMeta from '@/utils/meta'
- import { type PermissionsLevel } from '@/features/ide-react/types/permissions'
- const LOADED_AT = new Date()
- type IdeReactContextValue = {
- projectId: string
- eventEmitter: IdeEventEmitter
- startedFreeTrial: boolean
- setStartedFreeTrial: React.Dispatch<
- React.SetStateAction<IdeReactContextValue['startedFreeTrial']>
- >
- reportError: (error: any, meta?: Record<string, any>) => void
- projectJoined: boolean
- permissionsLevel: PermissionsLevel
- setPermissionsLevel: (permissionsLevel: PermissionsLevel) => void
- setOutOfSync: (value: boolean) => void
- }
- export const IdeReactContext = createContext<IdeReactContextValue | undefined>(
- undefined
- )
- function populateIdeReactScope(store: ReactScopeValueStore) {
- store.set('settings', {})
- }
- function populateProjectScope(store: ReactScopeValueStore) {
- store.allowNonExistentPath('project', true)
- }
- function populatePdfScope(store: ReactScopeValueStore) {
- store.allowNonExistentPath('pdf', true)
- }
- export function createReactScopeValueStore() {
- const scopeStore = new ReactScopeValueStore()
- // Populate the scope value store with default values that will be used by
- // nested contexts that refer to scope values. The ideal would be to leave
- // initialization of store values up to the nested context, which would keep
- // initialization code together with the context and would only populate
- // necessary values in the store, but this is simpler for now
- populateIdeReactScope(scopeStore)
- populateProjectScope(scopeStore)
- populatePdfScope(scopeStore)
- scopeStore.allowNonExistentPath('hasLintingError')
- return scopeStore
- }
- export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
- const projectId = getMeta('ol-project_id')
- const [scopeStore] = useState(() => createReactScopeValueStore())
- const [eventEmitter] = useState(createIdeEventEmitter)
- const [permissionsLevel, setPermissionsLevel] =
- useState<PermissionsLevel>('readOnly')
- const [outOfSync, setOutOfSync] = useState(false)
- const [scopeEventEmitter] = useState(
- () => new ReactScopeEventEmitter(eventEmitter)
- )
- const [unstableStore] = useState(() => {
- const store = new ReactScopeValueStore()
- // Add dummy editor.ready key for Writefull, that relies on this calling
- // back once after watching it
- store.set('editor.ready', undefined)
- return store
- })
- const [startedFreeTrial, setStartedFreeTrial] = useState(false)
- const release = getMeta('ol-ExposedSettings')?.sentryRelease ?? null
- // Set to true only after project:joined has fired and all its listeners have
- // been called
- const [projectJoined, setProjectJoined] = useState(false)
- const { socket, getSocketDebuggingInfo } = useConnectionContext()
- const reportError = useCallback(
- (error: any, meta?: Record<string, any>) => {
- const metadata = {
- ...meta,
- user_id: getMeta('ol-user_id'),
- project_id: projectId,
- client_now: new Date(),
- performance_now: performance.now(),
- release,
- client_load: LOADED_AT,
- spellCheckLanguage: scopeStore.get('project.spellCheckLanguage'),
- ...getSocketDebuggingInfo(),
- }
- const errorObj: Record<string, any> = {}
- if (typeof error === 'object') {
- for (const key of Object.getOwnPropertyNames(error)) {
- errorObj[key] = error[key]
- }
- } else if (typeof error === 'string') {
- errorObj.message = error
- }
- return postJSON('/error/client', {
- body: {
- error: errorObj,
- meta: metadata,
- },
- })
- },
- [release, projectId, getSocketDebuggingInfo, scopeStore]
- )
- // Populate scope values when joining project, then fire project:joined event
- useEffect(() => {
- function handleJoinProjectResponse({
- project: { rootDoc_id: rootDocId, ..._project },
- permissionsLevel,
- }: JoinProjectPayload) {
- const project = { ..._project, rootDocId }
- scopeStore.set('project', project)
- setPermissionsLevel(permissionsLevel)
- // Make watchers update immediately
- scopeStore.flushUpdates()
- eventEmitter.emit('project:joined', { project, permissionsLevel })
- setProjectJoined(true)
- }
- function handleMainBibliographyDocUpdated(payload: string) {
- scopeStore.set('project.mainBibliographyDoc_id', payload)
- }
- socket.on('joinProjectResponse', handleJoinProjectResponse)
- socket.on('mainBibliographyDocUpdated', handleMainBibliographyDocUpdated)
- return () => {
- socket.removeListener('joinProjectResponse', handleJoinProjectResponse)
- socket.removeListener(
- 'mainBibliographyDocUpdated',
- handleMainBibliographyDocUpdated
- )
- }
- }, [socket, eventEmitter, scopeStore])
- const ide = useMemo(() => {
- return {
- _id: projectId,
- socket,
- reportError,
- }
- }, [projectId, socket, reportError])
- const value = useMemo(
- () => ({
- eventEmitter,
- startedFreeTrial,
- setStartedFreeTrial,
- permissionsLevel: outOfSync ? 'readOnly' : permissionsLevel,
- setPermissionsLevel,
- setOutOfSync,
- projectId,
- reportError,
- projectJoined,
- }),
- [
- eventEmitter,
- outOfSync,
- permissionsLevel,
- projectId,
- projectJoined,
- reportError,
- startedFreeTrial,
- ]
- )
- return (
- <IdeReactContext.Provider value={value}>
- <IdeProvider
- ide={ide}
- scopeStore={scopeStore}
- scopeEventEmitter={scopeEventEmitter}
- unstableStore={unstableStore}
- >
- {children}
- </IdeProvider>
- </IdeReactContext.Provider>
- )
- }
- export function useIdeReactContext(): IdeReactContextValue {
- const context = useContext(IdeReactContext)
- if (!context) {
- throw new Error(
- 'useIdeReactContext is only available inside IdeReactProvider'
- )
- }
- return context
- }
|