ide-react-context.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import React, {
  2. createContext,
  3. useContext,
  4. useState,
  5. FC,
  6. useMemo,
  7. useEffect,
  8. useCallback,
  9. } from 'react'
  10. import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
  11. import { IdeProvider } from '@/shared/context/ide-context'
  12. import {
  13. createIdeEventEmitter,
  14. IdeEventEmitter,
  15. } from '@/features/ide-react/create-ide-event-emitter'
  16. import { JoinProjectPayload } from '@/features/ide-react/connection/join-project-payload'
  17. import { useConnectionContext } from '@/features/ide-react/context/connection-context'
  18. import { populateEditorScope } from '@/features/ide-react/scope-adapters/editor-manager-context-adapter'
  19. import { postJSON } from '@/infrastructure/fetch-json'
  20. import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
  21. import getMeta from '@/utils/meta'
  22. const LOADED_AT = new Date()
  23. type IdeReactContextValue = {
  24. projectId: string
  25. eventEmitter: IdeEventEmitter
  26. startedFreeTrial: boolean
  27. setStartedFreeTrial: React.Dispatch<
  28. React.SetStateAction<IdeReactContextValue['startedFreeTrial']>
  29. >
  30. reportError: (error: any, meta?: Record<string, any>) => void
  31. projectJoined: boolean
  32. }
  33. export const IdeReactContext = createContext<IdeReactContextValue | undefined>(
  34. undefined
  35. )
  36. function populateIdeReactScope(store: ReactScopeValueStore) {
  37. store.set('settings', {})
  38. }
  39. function populateProjectScope(store: ReactScopeValueStore) {
  40. store.allowNonExistentPath('project', true)
  41. store.set('permissionsLevel', 'readOnly')
  42. store.set('permissions', {
  43. read: true,
  44. write: false,
  45. admin: false,
  46. comment: true,
  47. })
  48. }
  49. function populatePdfScope(store: ReactScopeValueStore) {
  50. store.allowNonExistentPath('pdf', true)
  51. }
  52. export function createReactScopeValueStore(projectId: string) {
  53. const scopeStore = new ReactScopeValueStore()
  54. // Populate the scope value store with default values that will be used by
  55. // nested contexts that refer to scope values. The ideal would be to leave
  56. // initialization of store values up to the nested context, which would keep
  57. // initialization code together with the context and would only populate
  58. // necessary values in the store, but this is simpler for now
  59. populateIdeReactScope(scopeStore)
  60. populateEditorScope(scopeStore, projectId)
  61. populateProjectScope(scopeStore)
  62. populatePdfScope(scopeStore)
  63. scopeStore.allowNonExistentPath('hasLintingError')
  64. return scopeStore
  65. }
  66. export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
  67. const projectId = getMeta('ol-project_id')
  68. const [scopeStore] = useState(() => createReactScopeValueStore(projectId))
  69. const [eventEmitter] = useState(createIdeEventEmitter)
  70. const [scopeEventEmitter] = useState(
  71. () => new ReactScopeEventEmitter(eventEmitter)
  72. )
  73. const [startedFreeTrial, setStartedFreeTrial] = useState(false)
  74. const release = getMeta('ol-ExposedSettings')?.sentryRelease ?? null
  75. // Set to true only after project:joined has fired and all its listeners have
  76. // been called
  77. const [projectJoined, setProjectJoined] = useState(false)
  78. const { socket, getSocketDebuggingInfo } = useConnectionContext()
  79. const reportError = useCallback(
  80. (error: any, meta?: Record<string, any>) => {
  81. const metadata = {
  82. ...meta,
  83. user_id: getMeta('ol-user_id'),
  84. project_id: projectId,
  85. client_now: new Date(),
  86. performance_now: performance.now(),
  87. release,
  88. client_load: LOADED_AT,
  89. spellCheckLanguage: scopeStore.get('project.spellCheckLanguage'),
  90. ...getSocketDebuggingInfo(),
  91. }
  92. const errorObj: Record<string, any> = {}
  93. if (typeof error === 'object') {
  94. for (const key of Object.getOwnPropertyNames(error)) {
  95. errorObj[key] = error[key]
  96. }
  97. } else if (typeof error === 'string') {
  98. errorObj.message = error
  99. }
  100. return postJSON('/error/client', {
  101. body: {
  102. error: errorObj,
  103. meta: metadata,
  104. },
  105. })
  106. },
  107. [release, projectId, getSocketDebuggingInfo, scopeStore]
  108. )
  109. // Populate scope values when joining project, then fire project:joined event
  110. useEffect(() => {
  111. function handleJoinProjectResponse({
  112. project: { rootDoc_id: rootDocId, ..._project },
  113. permissionsLevel,
  114. }: JoinProjectPayload) {
  115. const project = { ..._project, rootDocId }
  116. scopeStore.set('project', project)
  117. scopeStore.set('permissionsLevel', permissionsLevel)
  118. // Make watchers update immediately
  119. scopeStore.flushUpdates()
  120. eventEmitter.emit('project:joined', { project, permissionsLevel })
  121. setProjectJoined(true)
  122. }
  123. function handleMainBibliographyDocUpdated(payload: string) {
  124. scopeStore.set('project.mainBibliographyDoc_id', payload)
  125. }
  126. socket.on('joinProjectResponse', handleJoinProjectResponse)
  127. socket.on('mainBibliographyDocUpdated', handleMainBibliographyDocUpdated)
  128. return () => {
  129. socket.removeListener('joinProjectResponse', handleJoinProjectResponse)
  130. socket.removeListener(
  131. 'mainBibliographyDocUpdated',
  132. handleMainBibliographyDocUpdated
  133. )
  134. }
  135. }, [socket, eventEmitter, scopeStore])
  136. const ide = useMemo(() => {
  137. return {
  138. _id: projectId,
  139. socket,
  140. reportError,
  141. }
  142. }, [projectId, socket, reportError])
  143. const value = useMemo(
  144. () => ({
  145. eventEmitter,
  146. startedFreeTrial,
  147. setStartedFreeTrial,
  148. projectId,
  149. reportError,
  150. projectJoined,
  151. }),
  152. [eventEmitter, projectId, projectJoined, reportError, startedFreeTrial]
  153. )
  154. return (
  155. <IdeReactContext.Provider value={value}>
  156. <IdeProvider
  157. ide={ide}
  158. scopeStore={scopeStore}
  159. scopeEventEmitter={scopeEventEmitter}
  160. >
  161. {children}
  162. </IdeProvider>
  163. </IdeReactContext.Provider>
  164. )
  165. }
  166. export function useIdeReactContext(): IdeReactContextValue {
  167. const context = useContext(IdeReactContext)
  168. if (!context) {
  169. throw new Error(
  170. 'useIdeReactContext is only available inside IdeReactProvider'
  171. )
  172. }
  173. return context
  174. }