layout-context.tsx 7.3 KB

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