layout-context.tsx 6.1 KB

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