editor-context.jsx 6.0 KB

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