layout-context.tsx 8.4 KB

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