Merge pull request #12962 from overleaf/td-history-loading-state-fix

History migration: Split updates and file diff loading states, move file restore load state out of context

GitOrigin-RevId: a994a1bdc8168043f045aaa3b7708bfce994d505
This commit is contained in:
Tim Down 2023-05-09 10:13:03 +01:00 committed by Copybot
parent 333b54c237
commit 5078f8ab43
7 changed files with 53 additions and 63 deletions

View file

@ -6,19 +6,16 @@ import { OwnerPaywallPrompt } from './owner-paywall-prompt'
import { NonOwnerPaywallPrompt } from './non-owner-paywall-prompt' import { NonOwnerPaywallPrompt } from './non-owner-paywall-prompt'
function AllHistoryList() { function AllHistoryList() {
const { const { updatesInfo, fetchNextBatchOfUpdates, currentUserIsOwner } =
updatesInfo, useHistoryContext()
loadingState, const updatesLoadingState = updatesInfo.loadingState
fetchNextBatchOfUpdates,
currentUserIsOwner,
} = useHistoryContext()
const { visibleUpdateCount, updates, atEnd } = updatesInfo const { visibleUpdateCount, updates, atEnd } = updatesInfo
const scrollerRef = useRef<HTMLDivElement>(null) const scrollerRef = useRef<HTMLDivElement>(null)
const bottomRef = useRef<HTMLDivElement>(null) const bottomRef = useRef<HTMLDivElement>(null)
const intersectionObserverRef = useRef<IntersectionObserver | null>(null) const intersectionObserverRef = useRef<IntersectionObserver | null>(null)
const [bottomVisible, setBottomVisible] = useState(false) const [bottomVisible, setBottomVisible] = useState(false)
const showPaywall = const showPaywall =
loadingState === 'ready' && updatesInfo.freeHistoryLimitHit updatesLoadingState === 'ready' && updatesInfo.freeHistoryLimitHit
const showOwnerPaywall = showPaywall && currentUserIsOwner const showOwnerPaywall = showPaywall && currentUserIsOwner
const showNonOwnerPaywall = showPaywall && !currentUserIsOwner const showNonOwnerPaywall = showPaywall && !currentUserIsOwner
const visibleUpdates = const visibleUpdates =
@ -27,7 +24,7 @@ function AllHistoryList() {
// Create an intersection observer that watches for any part of an element // Create an intersection observer that watches for any part of an element
// positioned at the bottom of the list to be visible // positioned at the bottom of the list to be visible
useEffect(() => { useEffect(() => {
if (loadingState === 'ready' && !intersectionObserverRef.current) { if (updatesLoadingState === 'ready' && !intersectionObserverRef.current) {
const scroller = scrollerRef.current const scroller = scrollerRef.current
const bottom = bottomRef.current const bottom = bottomRef.current
@ -48,13 +45,13 @@ function AllHistoryList() {
} }
} }
} }
}, [loadingState]) }, [updatesLoadingState])
useEffect(() => { useEffect(() => {
if (!atEnd && loadingState === 'ready' && bottomVisible) { if (!atEnd && updatesLoadingState === 'ready' && bottomVisible) {
fetchNextBatchOfUpdates() fetchNextBatchOfUpdates()
} }
}, [atEnd, bottomVisible, fetchNextBatchOfUpdates, loadingState]) }, [atEnd, bottomVisible, fetchNextBatchOfUpdates, updatesLoadingState])
// While updates are loading, remove the intersection observer and set // While updates are loading, remove the intersection observer and set
// bottomVisible to false. This is to avoid loading more updates immediately // bottomVisible to false. This is to avoid loading more updates immediately
@ -62,14 +59,14 @@ function AllHistoryList() {
// the intersection observer is asynchronous and won't have noticed that the // the intersection observer is asynchronous and won't have noticed that the
// bottom is no longer visible // bottom is no longer visible
useEffect(() => { useEffect(() => {
if (loadingState !== 'ready' && intersectionObserverRef.current) { if (updatesLoadingState !== 'ready' && intersectionObserverRef.current) {
setBottomVisible(false) setBottomVisible(false)
if (intersectionObserverRef.current) { if (intersectionObserverRef.current) {
intersectionObserverRef.current.disconnect() intersectionObserverRef.current.disconnect()
intersectionObserverRef.current = null intersectionObserverRef.current = null
} }
} }
}, [loadingState]) }, [updatesLoadingState])
return ( return (
<div ref={scrollerRef} className="history-all-versions-scroller"> <div ref={scrollerRef} className="history-all-versions-scroller">
@ -92,8 +89,8 @@ function AllHistoryList() {
</div> </div>
{showOwnerPaywall ? <OwnerPaywallPrompt /> : null} {showOwnerPaywall ? <OwnerPaywallPrompt /> : null}
{showNonOwnerPaywall ? <NonOwnerPaywallPrompt /> : null} {showNonOwnerPaywall ? <NonOwnerPaywallPrompt /> : null}
{loadingState === 'loadingInitial' || {updatesLoadingState === 'loadingInitial' ||
loadingState === 'loadingUpdates' ? ( updatesLoadingState === 'loadingUpdates' ? (
<LoadingSpinner /> <LoadingSpinner />
) : null} ) : null}
</div> </div>

View file

@ -9,8 +9,7 @@ import useAsync from '../../../../shared/hooks/use-async'
import ErrorMessage from '../error-message' import ErrorMessage from '../error-message'
function DiffView() { function DiffView() {
const { selection, projectId, loadingState } = useHistoryContext() const { selection, projectId, loadingFileDiffs } = useHistoryContext()
const loadingFileDiffs = loadingState === 'loadingFileDiffs'
const { isLoading, data, runAsync, error } = useAsync<DocDiffResponse>() const { isLoading, data, runAsync, error } = useAsync<DocDiffResponse>()
const { updateRange, selectedFile } = selection const { updateRange, selectedFile } = selection

View file

@ -1,6 +1,5 @@
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useHistoryContext } from '../../../context/history-context'
import { useRestoreDeletedFile } from '../../../context/hooks/use-restore-deleted-file' import { useRestoreDeletedFile } from '../../../context/hooks/use-restore-deleted-file'
import type { HistoryContextValue } from '../../../context/types/history-context-value' import type { HistoryContextValue } from '../../../context/types/history-context-value'
@ -12,21 +11,18 @@ export default function ToolbarRestoreFileButton({
selection, selection,
}: ToolbarRestoreFileButtonProps) { }: ToolbarRestoreFileButtonProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { loadingState } = useHistoryContext()
const onRestoreFile = useRestoreDeletedFile() const { restoreDeletedFile, isLoading } = useRestoreDeletedFile()
return ( return (
<Button <Button
className="btn-secondary history-react-toolbar-restore-file-button" className="btn-secondary history-react-toolbar-restore-file-button"
bsSize="xs" bsSize="xs"
bsStyle={null} bsStyle={null}
onClick={() => onRestoreFile(selection)} onClick={() => restoreDeletedFile(selection)}
disabled={loadingState === 'restoringFile'} disabled={isLoading}
> >
{loadingState === 'restoringFile' {isLoading ? `${t('restoring')}` : t('restore_file')}
? `${t('restoring')}`
: t('restore_file')}
</Button> </Button>
) )
} }

View file

@ -9,10 +9,10 @@ import ErrorMessage from './error-message'
const fileTreeContainer = document.getElementById('history-file-tree') const fileTreeContainer = document.getElementById('history-file-tree')
function Main() { function Main() {
const { loadingState, error } = useHistoryContext() const { updatesInfo, error } = useHistoryContext()
let content = null let content = null
if (loadingState === 'loadingInitial') { if (updatesInfo.loadingState === 'loadingInitial') {
content = <LoadingSpinner /> content = <LoadingSpinner />
} else if (error) { } else if (error) {
content = <ErrorMessage /> content = <ErrorMessage />

View file

@ -85,17 +85,19 @@ function useHistory() {
atEnd: false, atEnd: false,
freeHistoryLimitHit: false, freeHistoryLimitHit: false,
nextBeforeTimestamp: undefined, nextBeforeTimestamp: undefined,
loadingState: 'loadingInitial',
}) })
const [labels, setLabels] = useState<HistoryContextValue['labels']>(null) const [labels, setLabels] = useState<HistoryContextValue['labels']>(null)
const [labelsOnly, setLabelsOnly] = usePersistedState( const [labelsOnly, setLabelsOnly] = usePersistedState(
`history.userPrefs.showOnlyLabels.${projectId}`, `history.userPrefs.showOnlyLabels.${projectId}`,
false false
) )
const [loadingState, setLoadingState] = const [loadingFileDiffs, setLoadingFileDiffs] = useState(false)
useState<HistoryContextValue['loadingState']>('loadingInitial')
const [error, setError] = useState<HistoryContextValue['error']>(null) const [error, setError] = useState<HistoryContextValue['error']>(null)
const fetchNextBatchOfUpdates = useCallback(() => { const fetchNextBatchOfUpdates = useCallback(() => {
const updatesLoadingState = updatesInfo.loadingState
const loadUpdates = (updatesData: Update[]) => { const loadUpdates = (updatesData: Update[]) => {
const dateTimeNow = new Date() const dateTimeNow = new Date()
const timestamp24hoursAgo = dateTimeNow.setDate(dateTimeNow.getDate() - 1) const timestamp24hoursAgo = dateTimeNow.setDate(dateTimeNow.getDate() - 1)
@ -140,7 +142,10 @@ function useHistory() {
if ( if (
updatesInfo.atEnd || updatesInfo.atEnd ||
!(loadingState === 'loadingInitial' || loadingState === 'ready') !(
updatesLoadingState === 'loadingInitial' ||
updatesLoadingState === 'ready'
)
) { ) {
return return
} }
@ -150,9 +155,11 @@ function useHistory() {
) )
const labelsPromise = labels == null ? fetchLabels(projectId) : null const labelsPromise = labels == null ? fetchLabels(projectId) : null
setLoadingState( setUpdatesInfo({
loadingState === 'ready' ? 'loadingUpdates' : 'loadingInitial' ...updatesInfo,
) loadingState:
updatesLoadingState === 'ready' ? 'loadingUpdates' : 'loadingInitial',
})
Promise.all([updatesPromise, labelsPromise]) Promise.all([updatesPromise, labelsPromise])
.then(([{ updates: updatesData, nextBeforeTimestamp }, labels]) => { .then(([{ updates: updatesData, nextBeforeTimestamp }, labels]) => {
const lastUpdateToV = updatesData.length ? updatesData[0].toV : null const lastUpdateToV = updatesData.length ? updatesData[0].toV : null
@ -173,16 +180,14 @@ function useHistory() {
freeHistoryLimitHit, freeHistoryLimitHit,
atEnd, atEnd,
nextBeforeTimestamp, nextBeforeTimestamp,
loadingState: 'ready',
}) })
}) })
.catch(error => { .catch(error => {
setError(error) setError(error)
setUpdatesInfo({ ...updatesInfo, atEnd: true }) setUpdatesInfo({ ...updatesInfo, atEnd: true, loadingState: 'ready' })
}) })
.finally(() => { }, [updatesInfo, projectId, labels, userHasFullFeature])
setLoadingState('ready')
})
}, [loadingState, labels, projectId, userHasFullFeature, updatesInfo])
const resetSelection = useCallback(() => { const resetSelection = useCallback(() => {
setSelection(selectionInitialState) setSelection(selectionInitialState)
@ -203,12 +208,12 @@ function useHistory() {
// Load files when the update selection changes // Load files when the update selection changes
useEffect(() => { useEffect(() => {
if (!updateRange || loadingState !== 'ready' || !filesEmpty) { if (!updateRange || updatesInfo.loadingState !== 'ready' || !filesEmpty) {
return return
} }
const { fromV, toV } = updateRange const { fromV, toV } = updateRange
setLoadingState('loadingFileDiffs') setLoadingFileDiffs(true)
diffFiles(projectId, fromV, toV) diffFiles(projectId, fromV, toV)
.then(({ diff: files }) => { .then(({ diff: files }) => {
@ -236,7 +241,7 @@ function useHistory() {
setError(error) setError(error)
}) })
.finally(() => { .finally(() => {
setLoadingState('ready') setLoadingFileDiffs(false)
}) })
}, [ }, [
updateRange, updateRange,
@ -244,7 +249,7 @@ function useHistory() {
updates, updates,
comparing, comparing,
setError, setError,
loadingState, updatesInfo.loadingState,
filesEmpty, filesEmpty,
]) ])
@ -268,8 +273,8 @@ function useHistory() {
() => ({ () => ({
error, error,
setError, setError,
loadingState, loadingFileDiffs,
setLoadingState, setLoadingFileDiffs,
updatesInfo, updatesInfo,
setUpdatesInfo, setUpdatesInfo,
labels, labels,
@ -287,8 +292,8 @@ function useHistory() {
[ [
error, error,
setError, setError,
loadingState, loadingFileDiffs,
setLoadingState, setLoadingFileDiffs,
updatesInfo, updatesInfo,
setUpdatesInfo, setUpdatesInfo,
labels, labels,

View file

@ -9,17 +9,18 @@ import { useHistoryContext } from '../history-context'
import type { HistoryContextValue } from '../types/history-context-value' import type { HistoryContextValue } from '../types/history-context-value'
export function useRestoreDeletedFile() { export function useRestoreDeletedFile() {
const { runAsync } = useAsync() const { isLoading, runAsync } = useAsync()
const { setLoadingState, projectId, setError } = useHistoryContext() const { projectId, setError } = useHistoryContext()
const ide = useIdeContext() const ide = useIdeContext()
const { setView } = useLayoutContext() const { setView } = useLayoutContext()
return async (selection: HistoryContextValue['selection']) => { const restoreDeletedFile = async (
selection: HistoryContextValue['selection']
) => {
const { selectedFile } = selection const { selectedFile } = selection
if (selectedFile && selectedFile.pathname && isFileRemoved(selectedFile)) { if (selectedFile && selectedFile.pathname && isFileRemoved(selectedFile)) {
sendMB('history-v2-restore-deleted') sendMB('history-v2-restore-deleted')
setLoadingState('restoringFile')
await runAsync( await runAsync(
restoreFile(projectId, selectedFile) restoreFile(projectId, selectedFile)
@ -40,10 +41,9 @@ export function useRestoreDeletedFile() {
setView('editor') setView('editor')
}) })
.catch(error => setError(error)) .catch(error => setError(error))
.finally(() => {
setLoadingState('ready')
})
) )
} }
} }
return { restoreDeletedFile, isLoading }
} }

View file

@ -3,12 +3,7 @@ import { LoadedUpdate } from '../../services/types/update'
import { LoadedLabel } from '../../services/types/label' import { LoadedLabel } from '../../services/types/label'
import { Selection } from '../../services/types/selection' import { Selection } from '../../services/types/selection'
type LoadingState = type UpdatesLoadingState = 'loadingInitial' | 'loadingUpdates' | 'ready'
| 'loadingInitial'
| 'loadingUpdates'
| 'loadingFileDiffs'
| 'restoringFile'
| 'ready'
export type HistoryContextValue = { export type HistoryContextValue = {
updatesInfo: { updatesInfo: {
@ -17,16 +12,14 @@ export type HistoryContextValue = {
atEnd: boolean atEnd: boolean
nextBeforeTimestamp: number | undefined nextBeforeTimestamp: number | undefined
freeHistoryLimitHit: boolean freeHistoryLimitHit: boolean
loadingState: UpdatesLoadingState
} }
setUpdatesInfo: React.Dispatch< setUpdatesInfo: React.Dispatch<
React.SetStateAction<HistoryContextValue['updatesInfo']> React.SetStateAction<HistoryContextValue['updatesInfo']>
> >
userHasFullFeature: boolean userHasFullFeature: boolean
currentUserIsOwner: boolean currentUserIsOwner: boolean
loadingState: LoadingState loadingFileDiffs: boolean
setLoadingState: React.Dispatch<
React.SetStateAction<HistoryContextValue['loadingState']>
>
error: Nullable<unknown> error: Nullable<unknown>
setError: React.Dispatch<React.SetStateAction<HistoryContextValue['error']>> setError: React.Dispatch<React.SetStateAction<HistoryContextValue['error']>>
labels: Nullable<LoadedLabel[]> labels: Nullable<LoadedLabel[]>