layout-context.tsx 6.1 KB

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