ide-react-context.tsx 6.2 KB

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