editor-context.tsx 7.7 KB

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