editor-context.tsx 7.4 KB

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