ide-react-context.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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/context/editor-manager-context'
  21. import { postJSON } from '@/infrastructure/fetch-json'
  22. import { EventLog } from '@/features/ide-react/editor/event-log'
  23. import { populateSettingsScope } from '@/features/ide-react/scope-adapters/settings-adapter'
  24. import { populateOnlineUsersScope } from '@/features/ide-react/context/online-users-context'
  25. import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
  26. import { populateReferenceScope } from '@/features/ide-react/context/references-context'
  27. type IdeReactContextValue = {
  28. projectId: string
  29. eventEmitter: IdeEventEmitter
  30. eventLog: EventLog
  31. startedFreeTrial: boolean
  32. setStartedFreeTrial: React.Dispatch<
  33. React.SetStateAction<IdeReactContextValue['startedFreeTrial']>
  34. >
  35. reportError: (error: any, meta?: Record<string, any>) => void
  36. }
  37. const IdeReactContext = createContext<IdeReactContextValue | undefined>(
  38. undefined
  39. )
  40. function populateIdeReactScope(store: ReactScopeValueStore) {
  41. store.set('sync_tex_error', false)
  42. store.set('settings', window.userSettings)
  43. }
  44. function populateProjectScope(store: ReactScopeValueStore) {
  45. store.allowNonExistentPath('project', true)
  46. store.set('permissionsLevel', 'readOnly')
  47. }
  48. function populatePdfScope(store: ReactScopeValueStore) {
  49. store.allowNonExistentPath('pdf', true)
  50. }
  51. function populateFileTreeScope(store: ReactScopeValueStore) {
  52. store.set('docs', [])
  53. }
  54. function createReactScopeValueStore() {
  55. const scopeStore = new ReactScopeValueStore()
  56. // Populate the scope value store with default values that will be used by
  57. // nested contexts that refer to scope values. The ideal would be to leave
  58. // initialization of store values up to the nested context, which would keep
  59. // initialization code together with the context and would only populate
  60. // necessary values in the store, but this is simpler for now
  61. populateIdeReactScope(scopeStore)
  62. populateEditorScope(scopeStore)
  63. populateLayoutScope(scopeStore)
  64. populateProjectScope(scopeStore)
  65. populatePdfScope(scopeStore)
  66. populateSettingsScope(scopeStore)
  67. populateOnlineUsersScope(scopeStore)
  68. populateReferenceScope(scopeStore)
  69. populateFileTreeScope(scopeStore)
  70. scopeStore.allowNonExistentPath('hasLintingError')
  71. scopeStore.allowNonExistentPath('loadingThreads')
  72. return scopeStore
  73. }
  74. const projectId = window.project_id
  75. export const IdeReactProvider: FC = ({ children }) => {
  76. const [scopeStore] = useState(createReactScopeValueStore)
  77. const [eventEmitter] = useState(createIdeEventEmitter)
  78. const [scopeEventEmitter] = useState(
  79. () => new ReactScopeEventEmitter(eventEmitter)
  80. )
  81. const [eventLog] = useState(() => new EventLog())
  82. const [startedFreeTrial, setStartedFreeTrial] = useState(false)
  83. const { socket } = useConnectionContext()
  84. const reportError = useCallback(
  85. (error: any, meta?: Record<string, any>) => {
  86. const metadata = {
  87. ...meta,
  88. user_id: window.user_id,
  89. project_id: projectId,
  90. // @ts-ignore
  91. client_id: socket.socket.sessionid,
  92. // @ts-ignore
  93. transport: socket.socket.transport,
  94. client_now: new Date(),
  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. _csrf: window.csrfToken,
  109. },
  110. })
  111. },
  112. [socket.socket]
  113. )
  114. // Populate scope values when joining project, then fire project:joined event
  115. useEffect(() => {
  116. function handleJoinProjectResponse({
  117. project,
  118. permissionsLevel,
  119. }: JoinProjectPayload) {
  120. scopeStore.set('project', { rootDoc_id: null, ...project })
  121. scopeStore.set('permissionsLevel', permissionsLevel)
  122. // Make watchers update immediately
  123. scopeStore.flushUpdates()
  124. eventEmitter.emit('project:joined', { project, permissionsLevel })
  125. }
  126. socket.on('joinProjectResponse', handleJoinProjectResponse)
  127. return () => {
  128. socket.removeListener('joinProjectResponse', handleJoinProjectResponse)
  129. }
  130. }, [socket, eventEmitter, scopeStore])
  131. const ide = useMemo(() => {
  132. return {
  133. ...getMockIde(),
  134. socket,
  135. reportError,
  136. // TODO: MIGRATION: Remove this once it's no longer used
  137. fileTreeManager: {
  138. findEntityByPath: () => null,
  139. selectEntity: () => {},
  140. getPreviewByPath: () => null,
  141. getRootDocDirname: () => '',
  142. },
  143. }
  144. }, [socket, reportError])
  145. const value = useMemo(
  146. () => ({
  147. eventEmitter,
  148. eventLog,
  149. startedFreeTrial,
  150. setStartedFreeTrial,
  151. projectId,
  152. reportError,
  153. }),
  154. [eventEmitter, eventLog, reportError, startedFreeTrial]
  155. )
  156. return (
  157. <IdeReactContext.Provider value={value}>
  158. <IdeProvider
  159. ide={ide}
  160. scopeStore={scopeStore}
  161. scopeEventEmitter={scopeEventEmitter}
  162. >
  163. {children}
  164. </IdeProvider>
  165. </IdeReactContext.Provider>
  166. )
  167. }
  168. export function useIdeReactContext(): IdeReactContextValue {
  169. const context = useContext(IdeReactContext)
  170. if (!context) {
  171. throw new Error(
  172. 'useIdeReactContext is only available inside IdeReactProvider'
  173. )
  174. }
  175. return context
  176. }