layout-context.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import {
  2. createContext,
  3. useContext,
  4. useCallback,
  5. useMemo,
  6. useEffect,
  7. Dispatch,
  8. SetStateAction,
  9. FC,
  10. } from 'react'
  11. import useScopeValue from '../hooks/use-scope-value'
  12. import useDetachLayout from '../hooks/use-detach-layout'
  13. import localStorage from '../../infrastructure/local-storage'
  14. import getMeta from '../../utils/meta'
  15. import { debugConsole } from '@/utils/debugging'
  16. import { BinaryFile } from '@/features/file-view/types/binary-file'
  17. import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
  18. export type IdeLayout = 'sideBySide' | 'flat'
  19. export type IdeView = 'editor' | 'file' | 'pdf' | 'history'
  20. type LayoutContextValue = {
  21. reattach: () => void
  22. detach: () => void
  23. detachIsLinked: boolean
  24. detachRole: 'detacher' | 'detached' | null
  25. changeLayout: (newLayout: IdeLayout, newView?: IdeView) => void
  26. view: IdeView
  27. setView: (view: IdeView) => void
  28. chatIsOpen: boolean
  29. setChatIsOpen: Dispatch<SetStateAction<LayoutContextValue['chatIsOpen']>>
  30. reviewPanelOpen: boolean
  31. setReviewPanelOpen: Dispatch<
  32. SetStateAction<LayoutContextValue['reviewPanelOpen']>
  33. >
  34. leftMenuShown: boolean
  35. setLeftMenuShown: Dispatch<
  36. SetStateAction<LayoutContextValue['leftMenuShown']>
  37. >
  38. loadingStyleSheet: boolean
  39. setLoadingStyleSheet: Dispatch<
  40. SetStateAction<LayoutContextValue['loadingStyleSheet']>
  41. >
  42. pdfLayout: IdeLayout
  43. pdfPreviewOpen: boolean
  44. }
  45. const debugPdfDetach = getMeta('ol-debugPdfDetach')
  46. export const LayoutContext = createContext<LayoutContextValue | undefined>(
  47. undefined
  48. )
  49. function setLayoutInLocalStorage(pdfLayout: IdeLayout) {
  50. localStorage.setItem(
  51. 'pdf.layout',
  52. pdfLayout === 'sideBySide' ? 'split' : 'flat'
  53. )
  54. }
  55. export const LayoutProvider: FC = ({ children }) => {
  56. // what to show in the "flat" view (editor or pdf)
  57. const [view, _setView] = useScopeValue<IdeView>('ui.view')
  58. const [openFile] = useScopeValue<BinaryFile | null>('openFile')
  59. const historyToggleEmitter = useScopeEventEmitter('history:toggle', true)
  60. const setView = useCallback(
  61. (value: IdeView) => {
  62. _setView(oldValue => {
  63. // ensure that the "history:toggle" event is broadcast when switching in or out of history view
  64. if (value === 'history' || oldValue === 'history') {
  65. historyToggleEmitter()
  66. }
  67. if (value === 'editor' && openFile) {
  68. // if a file is currently opened, ensure the view is 'file' instead of
  69. // 'editor' when the 'editor' view is requested. This is to ensure
  70. // that the entity selected in the file tree is the one visible and
  71. // that docs don't take precedence over files.
  72. return 'file'
  73. }
  74. return value
  75. })
  76. },
  77. [_setView, openFile, historyToggleEmitter]
  78. )
  79. // whether the chat pane is open
  80. const [chatIsOpen, setChatIsOpen] = useScopeValue<boolean>('ui.chatOpen')
  81. // whether the review pane is open
  82. const [reviewPanelOpen, setReviewPanelOpen] =
  83. useScopeValue('ui.reviewPanelOpen')
  84. // whether the menu pane is open
  85. const [leftMenuShown, setLeftMenuShown] =
  86. useScopeValue<boolean>('ui.leftMenuShown')
  87. // whether to display the editor and preview side-by-side or full-width ("flat")
  88. const [pdfLayout, setPdfLayout] = useScopeValue<IdeLayout>('ui.pdfLayout')
  89. // whether stylesheet on theme is loading
  90. const [loadingStyleSheet, setLoadingStyleSheet] = useScopeValue<boolean>(
  91. 'ui.loadingStyleSheet'
  92. )
  93. const changeLayout = useCallback(
  94. (newLayout: IdeLayout, newView: IdeView = 'editor') => {
  95. setPdfLayout(newLayout)
  96. setView(newLayout === 'sideBySide' ? 'editor' : newView)
  97. setLayoutInLocalStorage(newLayout)
  98. },
  99. [setPdfLayout, setView]
  100. )
  101. const {
  102. reattach,
  103. detach,
  104. isLinking: detachIsLinking,
  105. isLinked: detachIsLinked,
  106. role: detachRole,
  107. isRedundant: detachIsRedundant,
  108. } = useDetachLayout()
  109. const pdfPreviewOpen =
  110. pdfLayout === 'sideBySide' || view === 'pdf' || detachRole === 'detacher'
  111. useEffect(() => {
  112. if (debugPdfDetach) {
  113. debugConsole.warn('Layout Effect', {
  114. detachIsRedundant,
  115. detachRole,
  116. detachIsLinking,
  117. detachIsLinked,
  118. })
  119. }
  120. if (detachRole !== 'detacher') return // not in a PDF detacher layout
  121. if (detachIsRedundant) {
  122. changeLayout('sideBySide')
  123. return
  124. }
  125. if (detachIsLinking || detachIsLinked) {
  126. // the tab is linked to a detached tab (or about to be linked); show
  127. // editor only
  128. changeLayout('flat', 'editor')
  129. }
  130. }, [
  131. detachIsRedundant,
  132. detachRole,
  133. detachIsLinking,
  134. detachIsLinked,
  135. changeLayout,
  136. ])
  137. const value = useMemo<LayoutContextValue>(
  138. () => ({
  139. reattach,
  140. detach,
  141. detachIsLinked,
  142. detachRole,
  143. changeLayout,
  144. chatIsOpen,
  145. leftMenuShown,
  146. pdfLayout,
  147. pdfPreviewOpen,
  148. reviewPanelOpen,
  149. loadingStyleSheet,
  150. setChatIsOpen,
  151. setLeftMenuShown,
  152. setPdfLayout,
  153. setReviewPanelOpen,
  154. setLoadingStyleSheet,
  155. setView,
  156. view,
  157. }),
  158. [
  159. reattach,
  160. detach,
  161. detachIsLinked,
  162. detachRole,
  163. changeLayout,
  164. chatIsOpen,
  165. leftMenuShown,
  166. pdfLayout,
  167. pdfPreviewOpen,
  168. reviewPanelOpen,
  169. loadingStyleSheet,
  170. setChatIsOpen,
  171. setLeftMenuShown,
  172. setPdfLayout,
  173. setReviewPanelOpen,
  174. setLoadingStyleSheet,
  175. setView,
  176. view,
  177. ]
  178. )
  179. return (
  180. <LayoutContext.Provider value={value}>{children}</LayoutContext.Provider>
  181. )
  182. }
  183. export function useLayoutContext() {
  184. const context = useContext(LayoutContext)
  185. if (!context) {
  186. throw new Error('useLayoutContext is only available inside LayoutProvider')
  187. }
  188. return context
  189. }