snapshot-context.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import {
  2. createContext,
  3. FC,
  4. useContext,
  5. useEffect,
  6. useMemo,
  7. useState,
  8. } from 'react'
  9. import { Snapshot } from 'overleaf-editor-core'
  10. import { useProjectContext } from '@/shared/context/project-context'
  11. import { debugConsole } from '@/utils/debugging'
  12. import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
  13. import { Folder } from '../../../../../types/folder'
  14. export const StubSnapshotUtils = {
  15. SnapshotUpdater: class SnapshotUpdater {
  16. // eslint-disable-next-line no-useless-constructor
  17. constructor(readonly projectId: string) {}
  18. refresh(): Promise<{ snapshot: Snapshot; snapshotVersion: number }> {
  19. throw new Error('not implemented')
  20. }
  21. abort(): void {
  22. throw new Error('not implemented')
  23. }
  24. },
  25. buildFileTree(snapshot: Snapshot): Folder {
  26. throw new Error('not implemented')
  27. },
  28. createFolder(_id: string, name: string): Folder {
  29. throw new Error('not implemented')
  30. },
  31. }
  32. const { SnapshotUpdater } =
  33. (importOverleafModules('snapshotUtils')[0]
  34. ?.import as typeof StubSnapshotUtils) || StubSnapshotUtils
  35. export type SnapshotLoadingState = '' | 'loading' | 'error'
  36. export const SnapshotContext = createContext<
  37. | {
  38. snapshotVersion: number
  39. snapshot?: Snapshot
  40. snapshotLoadingState: SnapshotLoadingState
  41. fileTreeFromHistory: boolean
  42. setFileTreeFromHistory: (v: boolean) => void
  43. }
  44. | undefined
  45. >(undefined)
  46. export const SnapshotProvider: FC<React.PropsWithChildren> = ({ children }) => {
  47. const { _id: projectId } = useProjectContext()
  48. const [snapshotLoadingState, setSnapshotLoadingState] =
  49. useState<SnapshotLoadingState>('')
  50. const [snapshotUpdater] = useState(() => new SnapshotUpdater(projectId))
  51. const [snapshot, setSnapshot] = useState<Snapshot>()
  52. const [snapshotVersion, setSnapshotVersion] = useState(-1)
  53. const [fileTreeFromHistory, setFileTreeFromHistory] = useState(false)
  54. useEffect(() => {
  55. if (!fileTreeFromHistory) return
  56. let stop = false
  57. let handle: number
  58. const refresh = () => {
  59. setSnapshotLoadingState('loading')
  60. snapshotUpdater
  61. .refresh()
  62. .then(({ snapshot, snapshotVersion }) => {
  63. setSnapshot(snapshot)
  64. setSnapshotVersion(snapshotVersion)
  65. setSnapshotLoadingState('')
  66. })
  67. .catch(err => {
  68. debugConsole.error(err)
  69. setSnapshotLoadingState('error')
  70. })
  71. .finally(() => {
  72. if (stop) return
  73. // use a chain of timeouts to avoid concurrent updates
  74. handle = window.setTimeout(refresh, 30_000)
  75. })
  76. }
  77. refresh()
  78. return () => {
  79. stop = true
  80. snapshotUpdater.abort()
  81. clearInterval(handle)
  82. }
  83. }, [projectId, fileTreeFromHistory, snapshotUpdater])
  84. const value = useMemo(
  85. () => ({
  86. snapshot,
  87. snapshotVersion,
  88. snapshotLoadingState,
  89. fileTreeFromHistory,
  90. setFileTreeFromHistory,
  91. }),
  92. [
  93. snapshot,
  94. snapshotVersion,
  95. snapshotLoadingState,
  96. fileTreeFromHistory,
  97. setFileTreeFromHistory,
  98. ]
  99. )
  100. return (
  101. <SnapshotContext.Provider value={value}>
  102. {children}
  103. </SnapshotContext.Provider>
  104. )
  105. }
  106. export function useSnapshotContext() {
  107. const context = useContext(SnapshotContext)
  108. if (!context) {
  109. throw new Error(
  110. 'useSnapshotContext is only available within SnapshotProvider'
  111. )
  112. }
  113. return context
  114. }