ide-react-context.tsx 6.3 KB

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