editor-context.tsx 6.9 KB

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