ide-react-context.tsx 6.4 KB

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