layout-context.tsx 6.5 KB

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