ide-react-context.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  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 { IdeProvider } from '@/shared/context/ide-context'
  12. import {
  13. createIdeEventEmitter,
  14. IdeEventEmitter,
  15. } from '@/features/ide-react/create-ide-event-emitter'
  16. import { JoinProjectPayload } from '@/features/ide-react/connection/join-project-payload'
  17. import { useConnectionContext } from '@/features/ide-react/context/connection-context'
  18. import { postJSON } from '@/infrastructure/fetch-json'
  19. import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
  20. import getMeta from '@/utils/meta'
  21. import { type PermissionsLevel } from '@/features/ide-react/types/permissions'
  22. import { useProjectContext } from '@/shared/context/project-context'
  23. import { ProjectMetadata } from '@/shared/context/types/project-metadata'
  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. permissionsLevel: PermissionsLevel
  35. setPermissionsLevel: (permissionsLevel: PermissionsLevel) => void
  36. setOutOfSync: (value: boolean) => void
  37. }
  38. export const IdeReactContext = createContext<IdeReactContextValue | undefined>(
  39. undefined
  40. )
  41. export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
  42. const projectId = getMeta('ol-project_id')
  43. const [eventEmitter] = useState(createIdeEventEmitter)
  44. const [permissionsLevel, setPermissionsLevel] =
  45. useState<PermissionsLevel>('readOnly')
  46. const [outOfSync, setOutOfSync] = useState(false)
  47. const [scopeEventEmitter] = useState(
  48. () => new ReactScopeEventEmitter(eventEmitter)
  49. )
  50. const [unstableStore] = useState(() => {
  51. const store = new ReactScopeValueStore()
  52. // Add dummy editor.ready key for Writefull, that relies on this calling
  53. // back once after watching it
  54. store.set('editor.ready', undefined)
  55. return store
  56. })
  57. const [startedFreeTrial, setStartedFreeTrial] = useState(false)
  58. const release = getMeta('ol-ExposedSettings')?.sentryRelease ?? null
  59. // Set to true only after project:joined has fired and all its listeners have
  60. // been called
  61. const [projectJoined, setProjectJoined] = useState(false)
  62. const { socket, getSocketDebuggingInfo } = useConnectionContext()
  63. const { joinProject, project } = useProjectContext()
  64. const spellCheckLanguage = project?.spellCheckLanguage
  65. const reportError = useCallback(
  66. (error: any, meta?: Record<string, any>) => {
  67. const metadata = {
  68. ...meta,
  69. user_id: getMeta('ol-user_id'),
  70. project_id: projectId,
  71. client_now: new Date(),
  72. performance_now: performance.now(),
  73. release,
  74. client_load: LOADED_AT,
  75. spellCheckLanguage,
  76. ...getSocketDebuggingInfo(),
  77. }
  78. const errorObj: Record<string, any> = {}
  79. if (typeof error === 'object') {
  80. for (const key of Object.getOwnPropertyNames(error)) {
  81. errorObj[key] = error[key]
  82. }
  83. } else if (typeof error === 'string') {
  84. errorObj.message = error
  85. }
  86. return postJSON('/error/client', {
  87. body: {
  88. error: errorObj,
  89. meta: metadata,
  90. },
  91. })
  92. },
  93. [release, projectId, getSocketDebuggingInfo, spellCheckLanguage]
  94. )
  95. // Populate scope values when joining project, then fire project:joined event
  96. useEffect(() => {
  97. function handleJoinProjectResponse({
  98. project: {
  99. rootDoc_id: rootDocId,
  100. publicAccesLevel: publicAccessLevel,
  101. ..._project
  102. },
  103. permissionsLevel,
  104. }: JoinProjectPayload) {
  105. const project = { ..._project, rootDocId, publicAccessLevel }
  106. // Cast the project from the payload as ProjectMetadata to ensure it has
  107. // the correct type for the context. It must be close enough because the
  108. // data structure hasn't changed and it worked previously. This type
  109. // coercion was previously sidestepped by adding the project to the scope
  110. // store, which does not enforce types.
  111. joinProject(project as unknown as ProjectMetadata)
  112. setPermissionsLevel(permissionsLevel)
  113. eventEmitter.emit('project:joined', { project, permissionsLevel })
  114. setProjectJoined(true)
  115. }
  116. socket.on('joinProjectResponse', handleJoinProjectResponse)
  117. return () => {
  118. socket.removeListener('joinProjectResponse', handleJoinProjectResponse)
  119. }
  120. }, [socket, eventEmitter, joinProject])
  121. const ide = useMemo(() => {
  122. return {
  123. _id: projectId,
  124. socket,
  125. reportError,
  126. }
  127. }, [projectId, socket, reportError])
  128. const value = useMemo(
  129. () => ({
  130. eventEmitter,
  131. startedFreeTrial,
  132. setStartedFreeTrial,
  133. permissionsLevel: outOfSync ? 'readOnly' : permissionsLevel,
  134. setPermissionsLevel,
  135. setOutOfSync,
  136. projectId,
  137. reportError,
  138. projectJoined,
  139. }),
  140. [
  141. eventEmitter,
  142. outOfSync,
  143. permissionsLevel,
  144. projectId,
  145. projectJoined,
  146. reportError,
  147. startedFreeTrial,
  148. ]
  149. )
  150. return (
  151. <IdeReactContext.Provider value={value}>
  152. <IdeProvider
  153. ide={ide}
  154. scopeEventEmitter={scopeEventEmitter}
  155. unstableStore={unstableStore}
  156. >
  157. {children}
  158. </IdeProvider>
  159. </IdeReactContext.Provider>
  160. )
  161. }
  162. export function useIdeReactContext(): IdeReactContextValue {
  163. const context = useContext(IdeReactContext)
  164. if (!context) {
  165. throw new Error(
  166. 'useIdeReactContext is only available inside IdeReactProvider'
  167. )
  168. }
  169. return context
  170. }