editor-context.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import {
  2. createContext,
  3. Dispatch,
  4. FC,
  5. SetStateAction,
  6. useCallback,
  7. useContext,
  8. useEffect,
  9. useMemo,
  10. useState,
  11. } from 'react'
  12. import useScopeValue from '../hooks/use-scope-value'
  13. import useBrowserWindow from '../hooks/use-browser-window'
  14. import { useIdeContext } from './ide-context'
  15. import { useProjectContext } from './project-context'
  16. import { useDetachContext } from './detach-context'
  17. import getMeta from '../../utils/meta'
  18. import { useUserContext } from './user-context'
  19. import { saveProjectSettings } from '@/features/editor-left-menu/utils/api'
  20. import { PermissionsLevel } from '@/features/ide-react/types/permissions'
  21. import { useModalsContext } from '@/features/ide-react/context/modals-context'
  22. export const EditorContext = createContext<
  23. | {
  24. cobranding?: {
  25. logoImgUrl: string
  26. brandVariationName: string
  27. brandVariationId: number
  28. brandId: number
  29. brandVariationHomeUrl: string
  30. publishGuideHtml?: string
  31. partner?: string
  32. brandedMenu?: boolean
  33. submitBtnHtml?: string
  34. }
  35. hasPremiumCompile?: boolean
  36. loading?: boolean
  37. renameProject: (newName: string) => void
  38. setPermissionsLevel: (permissionsLevel: PermissionsLevel) => void
  39. showSymbolPalette?: boolean
  40. toggleSymbolPalette?: () => void
  41. insertSymbol?: (symbol: string) => void
  42. isProjectOwner: boolean
  43. isRestrictedTokenMember?: boolean
  44. isPendingEditor: boolean
  45. permissionsLevel: 'readOnly' | 'readAndWrite' | 'owner'
  46. deactivateTutorial: (tutorial: string) => void
  47. inactiveTutorials: string[]
  48. currentPopup: string | null
  49. setCurrentPopup: Dispatch<SetStateAction<string | null>>
  50. setOutOfSync: (value: boolean) => void
  51. hasPremiumSuggestion: boolean
  52. setHasPremiumSuggestion: (value: boolean) => void
  53. setPremiumSuggestionResetDate: (date: Date) => void
  54. premiumSuggestionResetDate: Date
  55. }
  56. | undefined
  57. >(undefined)
  58. export const EditorProvider: FC = ({ children }) => {
  59. const ide = useIdeContext()
  60. const { id: userId, featureUsage } = useUserContext()
  61. const { role } = useDetachContext()
  62. const { showGenericMessageModal } = useModalsContext()
  63. const { owner, features, _id: projectId, members } = useProjectContext()
  64. const cobranding = useMemo(() => {
  65. const brandVariation = getMeta('ol-brandVariation')
  66. return (
  67. brandVariation && {
  68. logoImgUrl: brandVariation.logo_url,
  69. brandVariationName: brandVariation.name,
  70. brandVariationId: brandVariation.id,
  71. brandId: brandVariation.brand_id,
  72. brandVariationHomeUrl: brandVariation.home_url,
  73. publishGuideHtml: brandVariation.publish_guide_html,
  74. partner: brandVariation.partner,
  75. brandedMenu: brandVariation.branded_menu,
  76. submitBtnHtml: brandVariation.submit_button_html,
  77. }
  78. )
  79. }, [])
  80. const [loading] = useScopeValue('state.loading')
  81. const [projectName, setProjectName] = useScopeValue('project.name')
  82. const [permissionsLevel, setPermissionsLevel] =
  83. useScopeValue('permissionsLevel')
  84. const [outOfSync, setOutOfSync] = useState(false)
  85. const [showSymbolPalette] = useScopeValue('editor.showSymbolPalette')
  86. const [toggleSymbolPalette] = useScopeValue('editor.toggleSymbolPalette')
  87. const [inactiveTutorials, setInactiveTutorials] = useState(
  88. () => getMeta('ol-inactiveTutorials') || []
  89. )
  90. const [currentPopup, setCurrentPopup] = useState<string | null>(null)
  91. const [hasPremiumSuggestion, setHasPremiumSuggestion] = useState<boolean>(
  92. () => {
  93. return Boolean(
  94. featureUsage?.aiErrorAssistant &&
  95. featureUsage?.aiErrorAssistant.remainingUsage > 0
  96. )
  97. }
  98. )
  99. const [premiumSuggestionResetDate, setPremiumSuggestionResetDate] =
  100. useState<Date>(() => {
  101. return featureUsage?.aiErrorAssistant.resetDate
  102. ? new Date(featureUsage.aiErrorAssistant.resetDate)
  103. : new Date()
  104. })
  105. const isPendingEditor = useMemo(
  106. () =>
  107. members?.some(member => member._id === userId && member.pendingEditor),
  108. [members, userId]
  109. )
  110. const deactivateTutorial = useCallback(
  111. tutorialKey => {
  112. setInactiveTutorials([...inactiveTutorials, tutorialKey])
  113. },
  114. [inactiveTutorials]
  115. )
  116. useEffect(() => {
  117. if (ide?.socket) {
  118. ide.socket.on('projectNameUpdated', setProjectName)
  119. return () =>
  120. ide.socket.removeListener('projectNameUpdated', setProjectName)
  121. }
  122. }, [ide?.socket, setProjectName])
  123. const renameProject = useCallback(
  124. (newName: string) => {
  125. setProjectName((oldName: string) => {
  126. if (oldName !== newName) {
  127. saveProjectSettings(projectId, { name: newName }).catch(
  128. (response: any) => {
  129. setProjectName(oldName)
  130. const { data, status } = response
  131. showGenericMessageModal(
  132. 'Error renaming project',
  133. status === 400 ? data : 'Please try again in a moment'
  134. )
  135. }
  136. )
  137. }
  138. return newName
  139. })
  140. },
  141. [setProjectName, projectId, showGenericMessageModal]
  142. )
  143. const { setTitle } = useBrowserWindow()
  144. useEffect(() => {
  145. const parts = []
  146. if (role === 'detached') {
  147. parts.push('[PDF]')
  148. }
  149. if (projectName) {
  150. parts.push(projectName)
  151. parts.push('-')
  152. }
  153. parts.push('Online LaTeX Editor')
  154. parts.push(getMeta('ol-ExposedSettings').appName)
  155. const title = parts.join(' ')
  156. setTitle(title)
  157. }, [projectName, setTitle, role])
  158. const insertSymbol = useCallback((symbol: string) => {
  159. window.dispatchEvent(
  160. new CustomEvent('editor:insert-symbol', {
  161. detail: symbol,
  162. })
  163. )
  164. }, [])
  165. const value = useMemo(
  166. () => ({
  167. cobranding,
  168. hasPremiumCompile: features?.compileGroup === 'priority',
  169. loading,
  170. renameProject,
  171. permissionsLevel: outOfSync ? 'readOnly' : permissionsLevel,
  172. setPermissionsLevel,
  173. isProjectOwner: owner?._id === userId,
  174. isRestrictedTokenMember: getMeta('ol-isRestrictedTokenMember'),
  175. isPendingEditor,
  176. showSymbolPalette,
  177. toggleSymbolPalette,
  178. insertSymbol,
  179. inactiveTutorials,
  180. deactivateTutorial,
  181. currentPopup,
  182. setCurrentPopup,
  183. setOutOfSync,
  184. hasPremiumSuggestion,
  185. setHasPremiumSuggestion,
  186. premiumSuggestionResetDate,
  187. setPremiumSuggestionResetDate,
  188. }),
  189. [
  190. cobranding,
  191. features?.compileGroup,
  192. owner,
  193. userId,
  194. loading,
  195. renameProject,
  196. permissionsLevel,
  197. setPermissionsLevel,
  198. isPendingEditor,
  199. showSymbolPalette,
  200. toggleSymbolPalette,
  201. insertSymbol,
  202. inactiveTutorials,
  203. deactivateTutorial,
  204. currentPopup,
  205. setCurrentPopup,
  206. outOfSync,
  207. setOutOfSync,
  208. hasPremiumSuggestion,
  209. setHasPremiumSuggestion,
  210. premiumSuggestionResetDate,
  211. setPremiumSuggestionResetDate,
  212. ]
  213. )
  214. return (
  215. <EditorContext.Provider value={value}>{children}</EditorContext.Provider>
  216. )
  217. }
  218. export function useEditorContext() {
  219. const context = useContext(EditorContext)
  220. if (!context) {
  221. throw new Error('useEditorContext is only available inside EditorProvider')
  222. }
  223. return context
  224. }