editor-context.js 5.7 KB

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