ide-react-context.tsx 6.1 KB

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