layout-context.tsx 8.0 KB

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