layout-context.tsx 8.3 KB

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