editor-context.tsx 6.7 KB

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