layout-context.tsx 8.1 KB

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