settings-modal-context.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. import {
  2. createContext,
  3. FC,
  4. useCallback,
  5. useContext,
  6. useMemo,
  7. useState,
  8. } from 'react'
  9. import { useLayoutContext } from '@/shared/context/layout-context'
  10. import AutoCloseBracketsSetting from '@/features/settings/components/editor-settings/auto-close-brackets-setting'
  11. import AutoCompleteSetting from '@/features/settings/components/editor-settings/auto-complete-setting'
  12. import CodeCheckSetting from '@/features/settings/components/editor-settings/code-check-setting'
  13. import PreviewTabsSetting from '@/features/settings/components/editor-settings/preview-tabs-setting'
  14. import KeybindingSetting from '@/features/settings/components/editor-settings/keybinding-setting'
  15. import PDFViewerSetting from '@/features/settings/components/editor-settings/pdf-viewer-setting'
  16. import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
  17. import SpellCheckSetting from '@/features/settings/components/editor-settings/spell-check-setting'
  18. import DictionarySetting from '@/features/settings/components/editor-settings/dictionary-setting'
  19. import { useTranslation } from 'react-i18next'
  20. import BreadcrumbsSetting from '@/features/settings/components/editor-settings/breadcrumbs-setting'
  21. import NonBlinkingCursorSetting from '@/features/settings/components/editor-settings/non-blinking-cursor-setting'
  22. import MathPreviewSetting from '@/features/settings/components/editor-settings/math-preview-setting'
  23. import RootDocumentSetting from '@/features/settings/components/compiler-settings/root-document-setting'
  24. import CompilerSetting from '@/features/settings/components/compiler-settings/compiler-setting'
  25. import ImageNameSetting from '@/features/settings/components/compiler-settings/image-name-setting'
  26. import DraftSetting from '@/features/settings/components/compiler-settings/draft-setting'
  27. import StopOnFirstErrorSetting from '@/features/settings/components/compiler-settings/stop-on-first-error-setting'
  28. import AutoCompileSetting from '@/features/settings/components/compiler-settings/auto-compile-setting'
  29. import OverallThemeSetting from '@/features/settings/components/appearance-settings/overall-theme-setting'
  30. import EditorThemeSetting from '@/features/settings/components/appearance-settings/editor-theme-setting'
  31. import FontSizeSetting from '@/features/settings/components/appearance-settings/font-size-setting'
  32. import LineHeightSetting from '@/features/settings/components/appearance-settings/line-height-setting'
  33. import FontFamilySetting from '@/features/settings/components/appearance-settings/font-family-setting'
  34. import DarkModePdfSetting from '@/features/settings/components/appearance-settings/dark-mode-pdf-setting'
  35. import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
  36. import { useFeatureFlag } from '@/shared/context/split-test-context'
  37. import ProjectNotificationsSetting from '@/features/settings/components/editor-settings/project-notifications-setting'
  38. import getMeta from '@/utils/meta'
  39. import type {
  40. SettingsEntry,
  41. SettingsSection,
  42. SettingsSectionHook,
  43. } from '@/features/settings/context/types'
  44. import EditorTabsSetting from '../components/editor-settings/editor-tabs-setting'
  45. import FloatingMenuSetting from '../components/editor-settings/floating-menu-setting'
  46. import useEventListener from '@/shared/hooks/use-event-listener'
  47. const [referenceSearchSettingModule] = importOverleafModules(
  48. 'referenceSearchSetting'
  49. )
  50. const ReferenceSearchSetting = referenceSearchSettingModule?.import.default
  51. const editorTabExtraSectionHooks: SettingsSectionHook[] = importOverleafModules(
  52. 'settingsModalEditorTabSections'
  53. )
  54. .map((m: any) => m?.import?.default)
  55. .filter((h: unknown): h is SettingsSectionHook => typeof h === 'function')
  56. const spellcheckExtraSectionHooks: SettingsSectionHook[] =
  57. importOverleafModules('settingsModalSpellcheckSections')
  58. .map((m: any) => m?.import?.default)
  59. .filter((h: unknown): h is SettingsSectionHook => typeof h === 'function')
  60. const useSlotSections = (hooks: SettingsSectionHook[]): SettingsSection[] =>
  61. hooks.map(hook => hook()).filter((s): s is SettingsSection => s != null)
  62. type SettingsModalState = {
  63. show: boolean
  64. setShow: (shown: boolean) => void
  65. activeTab: string | null | undefined
  66. setActiveTab: (tab: string | null | undefined) => void
  67. settingsTabs: SettingsEntry[]
  68. settingToTabMap: Map<string, string>
  69. settingToFocus?: string
  70. }
  71. export const SettingsModalContext = createContext<
  72. SettingsModalState | undefined
  73. >(undefined)
  74. export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
  75. children,
  76. }) => {
  77. const { t } = useTranslation()
  78. const { isOverleaf } = getMeta('ol-ExposedSettings')
  79. const { overallTheme, floatingMenu } = useProjectSettingsContext()
  80. const [settingToFocus, setSettingToFocus] = useState<string | undefined>(
  81. undefined
  82. )
  83. useEventListener(
  84. 'ui.focus-setting',
  85. useCallback((event: CustomEvent<string | undefined>) => {
  86. setSettingToFocus(event.detail)
  87. }, [])
  88. )
  89. const { settingsShown, setSettingsShown } = useLayoutContext()
  90. const hasEmailNotifications = useFeatureFlag('email-notifications')
  91. const hasEditorTabs = useFeatureFlag('editor-tabs')
  92. const hasToolbarMigration = useFeatureFlag('writefull-toolbar-migration')
  93. const editorTabExtraSections = useSlotSections(editorTabExtraSectionHooks)
  94. const spellcheckExtraSections = useSlotSections(spellcheckExtraSectionHooks)
  95. const allSettingsTabs: SettingsEntry[] = useMemo(
  96. () => [
  97. {
  98. key: 'editor',
  99. title: t('editor'),
  100. icon: 'code',
  101. sections: [
  102. {
  103. key: 'general',
  104. settings: [
  105. {
  106. key: 'autoComplete',
  107. component: <AutoCompleteSetting />,
  108. },
  109. {
  110. key: 'autoPairDelimiters',
  111. component: <AutoCloseBracketsSetting />,
  112. },
  113. {
  114. key: 'nonBlinkingCursor',
  115. component: <NonBlinkingCursorSetting />,
  116. },
  117. {
  118. key: 'syntaxValidation',
  119. component: <CodeCheckSetting />,
  120. },
  121. {
  122. key: 'editorTabs',
  123. component: <EditorTabsSetting />,
  124. hidden: !hasEditorTabs,
  125. },
  126. {
  127. key: 'previewTabs',
  128. component: <PreviewTabsSetting />,
  129. hidden: !hasEditorTabs,
  130. },
  131. {
  132. key: 'mode',
  133. component: <KeybindingSetting />,
  134. },
  135. {
  136. key: 'pdfViewer',
  137. component: <PDFViewerSetting />,
  138. },
  139. {
  140. key: 'write-and-cite-settings',
  141. component: <ReferenceSearchSetting />,
  142. hidden: !ReferenceSearchSetting,
  143. },
  144. {
  145. key: 'floating-menu',
  146. component: <FloatingMenuSetting />,
  147. hidden: !hasToolbarMigration && floatingMenu,
  148. },
  149. ],
  150. },
  151. {
  152. key: 'tools',
  153. title: t('tools'),
  154. settings: [
  155. {
  156. key: 'breadcrumbs-setting',
  157. component: <BreadcrumbsSetting />,
  158. },
  159. {
  160. key: 'mathPreview',
  161. component: <MathPreviewSetting />,
  162. },
  163. ],
  164. },
  165. ...editorTabExtraSections,
  166. ],
  167. },
  168. {
  169. key: 'spelling_and_language',
  170. title: t('spelling_and_language'),
  171. icon: 'spellcheck',
  172. sections: [
  173. {
  174. key: 'spellcheck',
  175. title: t('spellcheck'),
  176. settings: [
  177. {
  178. key: 'spellCheckLanguage',
  179. component: <SpellCheckSetting />,
  180. },
  181. {
  182. key: 'dictionary-settings',
  183. component: <DictionarySetting />,
  184. },
  185. ],
  186. },
  187. ...spellcheckExtraSections,
  188. ],
  189. },
  190. {
  191. key: 'compiler',
  192. title: t('compiler'),
  193. icon: 'picture_as_pdf',
  194. sections: [
  195. {
  196. key: 'general',
  197. settings: [
  198. {
  199. key: 'rootDocId',
  200. component: <RootDocumentSetting />,
  201. },
  202. {
  203. key: 'compiler',
  204. component: <CompilerSetting />,
  205. },
  206. {
  207. key: 'imageName',
  208. component: <ImageNameSetting />,
  209. },
  210. {
  211. key: 'draft',
  212. component: <DraftSetting />,
  213. },
  214. {
  215. key: 'stopOnFirstError',
  216. component: <StopOnFirstErrorSetting />,
  217. },
  218. {
  219. key: 'autoCompile',
  220. component: <AutoCompileSetting />,
  221. },
  222. ],
  223. },
  224. ],
  225. },
  226. {
  227. key: 'appearance',
  228. title: t('appearance'),
  229. icon: 'brush',
  230. sections: [
  231. {
  232. key: 'general',
  233. settings: [
  234. {
  235. key: 'overallTheme',
  236. component: <OverallThemeSetting />,
  237. },
  238. {
  239. key: 'editorTheme',
  240. component: <EditorThemeSetting />,
  241. },
  242. {
  243. key: 'pdfDarkMode',
  244. component: <DarkModePdfSetting />,
  245. hidden: overallTheme === 'light-',
  246. },
  247. {
  248. key: 'fontSize',
  249. component: <FontSizeSetting />,
  250. },
  251. {
  252. key: 'fontFamily',
  253. component: <FontFamilySetting />,
  254. },
  255. {
  256. key: 'lineHeight',
  257. component: <LineHeightSetting />,
  258. },
  259. ],
  260. },
  261. ],
  262. },
  263. {
  264. key: 'project_notifications',
  265. title: t('project_notifications'),
  266. icon: 'notifications' as const,
  267. sections: [
  268. {
  269. key: 'general',
  270. settings: [
  271. {
  272. key: 'projectNotifications',
  273. component: <ProjectNotificationsSetting />,
  274. },
  275. ],
  276. },
  277. ],
  278. hidden: !hasEmailNotifications,
  279. },
  280. {
  281. key: 'account_settings',
  282. title: t('account_settings'),
  283. icon: 'settings',
  284. href: '/user/settings',
  285. },
  286. {
  287. key: 'subscription',
  288. title: t('subscription'),
  289. icon: 'account_balance',
  290. href: '/user/subscription',
  291. hidden: !isOverleaf,
  292. },
  293. ],
  294. [
  295. t,
  296. hasEditorTabs,
  297. overallTheme,
  298. hasEmailNotifications,
  299. isOverleaf,
  300. editorTabExtraSections,
  301. spellcheckExtraSections,
  302. hasToolbarMigration,
  303. floatingMenu,
  304. ]
  305. )
  306. const settingsTabs = useMemo(
  307. () => allSettingsTabs.filter(tab => !tab.hidden),
  308. [allSettingsTabs]
  309. )
  310. const settingToTabMap = useMemo(() => {
  311. const map = new Map<string, string>()
  312. settingsTabs
  313. .filter(t => 'sections' in t)
  314. .forEach(tab => {
  315. tab.sections.forEach(section => {
  316. section.settings.forEach(setting => {
  317. map.set(setting.key, tab.key)
  318. })
  319. })
  320. })
  321. return map
  322. }, [settingsTabs])
  323. const [activeTab, setActiveTab] = useState<string | null | undefined>(
  324. settingsTabs[0]?.key
  325. )
  326. const value = useMemo(
  327. () => ({
  328. show: settingsShown,
  329. setShow: setSettingsShown,
  330. activeTab,
  331. setActiveTab,
  332. settingsTabs,
  333. settingToTabMap,
  334. settingToFocus,
  335. }),
  336. [
  337. settingsShown,
  338. setSettingsShown,
  339. activeTab,
  340. setActiveTab,
  341. settingsTabs,
  342. settingToTabMap,
  343. settingToFocus,
  344. ]
  345. )
  346. return (
  347. <SettingsModalContext.Provider value={value}>
  348. {children}
  349. </SettingsModalContext.Provider>
  350. )
  351. }
  352. export const useSettingsModalContext = () => {
  353. const value = useContext(SettingsModalContext)
  354. if (!value) {
  355. throw new Error(
  356. `useSettingsModalContext is only available inside SettingsModalProvider`
  357. )
  358. }
  359. return value
  360. }