mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-20 07:33:37 +00:00
Merge pull request #16335 from overleaf/ae-real-time-down
[ide-react] Improve handling of lost connection GitOrigin-RevId: 89b641b2beca4f9de65551e6873b3c8c11bb1695
This commit is contained in:
parent
ecfa15cf57
commit
eb3e5037f8
9 changed files with 88 additions and 12 deletions
|
@ -4,6 +4,8 @@ import { useConnectionContext } from '@/features/ide-react/context/connection-co
|
|||
import { debugging } from '@/utils/debugging'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import useScopeValue from '@/shared/hooks/use-scope-value'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useGlobalAlertsContainer } from '@/features/ide-react/context/global-alerts-context'
|
||||
|
||||
export function Alerts() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -14,11 +16,16 @@ export function Alerts() {
|
|||
tryReconnectNow,
|
||||
secondsUntilReconnect,
|
||||
} = useConnectionContext()
|
||||
const globalAlertsContainer = useGlobalAlertsContainer()
|
||||
|
||||
const [synctexError] = useScopeValue('sync_tex_error')
|
||||
|
||||
return (
|
||||
<div className="global-alerts">
|
||||
if (!globalAlertsContainer) {
|
||||
return null
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<>
|
||||
{connectionState.forceDisconnected ? (
|
||||
<Alert bsStyle="danger" className="small">
|
||||
<strong>{t('disconnected')}</strong>
|
||||
|
@ -67,6 +74,7 @@ export function Alerts() {
|
|||
<strong>Connected: {isConnected.toString()}</strong>
|
||||
</Alert>
|
||||
) : null}
|
||||
</div>
|
||||
</>,
|
||||
globalAlertsContainer
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ export function FileTree() {
|
|||
const user = useUserContext()
|
||||
const { indexAllReferences } = useReferencesContext()
|
||||
const { setStartedFreeTrial } = useIdeReactContext()
|
||||
const { isConnected } = useConnectionContext()
|
||||
const { isConnected, connectionState } = useConnectionContext()
|
||||
const { handleFileTreeInit, handleFileTreeSelect, handleFileTreeDelete } =
|
||||
useFileTreeOpenContext()
|
||||
|
||||
|
@ -37,7 +37,7 @@ export function FileTree() {
|
|||
reindexReferences={reindexReferences}
|
||||
setRefProviderEnabled={setRefProviderEnabled}
|
||||
setStartedFreeTrial={setStartedFreeTrial}
|
||||
isConnected={isConnected}
|
||||
isConnected={isConnected || connectionState.reconnectAt !== null}
|
||||
onInit={handleFileTreeInit}
|
||||
onSelect={handleFileTreeSelect}
|
||||
onDelete={handleFileTreeDelete}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { useEditingSessionHeartbeat } from '@/features/ide-react/hooks/use-editi
|
|||
import { useRegisterUserActivity } from '@/features/ide-react/hooks/use-register-user-activity'
|
||||
import { useHasLintingError } from '@/features/ide-react/hooks/use-has-linting-error'
|
||||
import { Modals } from '@/features/ide-react/components/modals/modals'
|
||||
import { GlobalAlertsProvider } from '@/features/ide-react/context/global-alerts-context'
|
||||
|
||||
export default function IdePage() {
|
||||
useLayoutEventTracking() // sent event when the layout changes
|
||||
|
@ -18,11 +19,11 @@ export default function IdePage() {
|
|||
useOpenFile() // create ide.binaryFilesManager (TODO: move to the history file restore component)
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlobalAlertsProvider>
|
||||
<Alerts />
|
||||
<Modals />
|
||||
<EditorLeftMenu />
|
||||
<MainLayout />
|
||||
</>
|
||||
</GlobalAlertsProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,14 +6,14 @@ import { useTranslation } from 'react-i18next'
|
|||
export const UnsavedDocsAlert: FC<{ unsavedDocs: Map<string, number> }> = ({
|
||||
unsavedDocs,
|
||||
}) => (
|
||||
<div className="global-alerts">
|
||||
<>
|
||||
{[...unsavedDocs.entries()].map(
|
||||
([docId, seconds]) =>
|
||||
seconds > 8 && (
|
||||
<UnsavedDocAlert key={docId} docId={docId} seconds={seconds} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
const UnsavedDocAlert: FC<{ docId: string; seconds: number }> = ({
|
||||
|
|
|
@ -8,6 +8,7 @@ export const UnsavedDocsLockedModal: FC = () => {
|
|||
|
||||
return (
|
||||
<AccessibleModal
|
||||
show
|
||||
onHide={() => {}} // It's not possible to hide this modal, but it's a required prop
|
||||
className="lock-editor-modal"
|
||||
backdrop={false}
|
||||
|
|
|
@ -5,6 +5,8 @@ import { PermissionsLevel } from '@/features/ide-react/types/permissions'
|
|||
import { UnsavedDocsLockedModal } from '@/features/ide-react/components/unsaved-docs/unsaved-docs-locked-modal'
|
||||
import { UnsavedDocsAlert } from '@/features/ide-react/components/unsaved-docs/unsaved-docs-alert'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useGlobalAlertsContainer } from '@/features/ide-react/context/global-alerts-context'
|
||||
|
||||
const MAX_UNSAVED_SECONDS = 15 // lock the editor after this time if unsaved
|
||||
|
||||
|
@ -13,6 +15,7 @@ export const UnsavedDocs: FC = () => {
|
|||
const { permissionsLevel, setPermissionsLevel } = useEditorContext()
|
||||
const [isLocked, setIsLocked] = useState(false)
|
||||
const [unsavedDocs, setUnsavedDocs] = useState(new Map<string, number>())
|
||||
const globalAlertsContainer = useGlobalAlertsContainer()
|
||||
|
||||
// always contains the latest value
|
||||
const previousUnsavedDocsRef = useRef(unsavedDocs)
|
||||
|
@ -99,7 +102,12 @@ export const UnsavedDocs: FC = () => {
|
|||
return (
|
||||
<>
|
||||
{isLocked && <UnsavedDocsLockedModal />}
|
||||
{unsavedDocs.size > 0 && <UnsavedDocsAlert unsavedDocs={unsavedDocs} />}
|
||||
{unsavedDocs.size > 0 &&
|
||||
globalAlertsContainer &&
|
||||
createPortal(
|
||||
<UnsavedDocsAlert unsavedDocs={unsavedDocs} />,
|
||||
globalAlertsContainer
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@ const TWO_MINUTES_IN_MS = 2 * 60 * 1000
|
|||
const DISCONNECT_AFTER_MS = ONE_HOUR_IN_MS * 24
|
||||
|
||||
const CONNECTION_ERROR_RECONNECT_DELAY = 1000
|
||||
const USER_ACTIVITY_RECONNECT_DELAY = 1000
|
||||
const USER_ACTIVITY_RECONNECT_NOW_DELAY = 1000
|
||||
const USER_ACTIVITY_RECONNECT_DELAY = 5000
|
||||
const JOIN_PROJECT_RATE_LIMITED_DELAY = 15 * 1000
|
||||
|
||||
const RECONNECT_GRACEFULLY_RETRY_INTERVAL_MS = 5000
|
||||
|
@ -95,7 +96,7 @@ export class ConnectionManager extends Emitter<Events> {
|
|||
}
|
||||
|
||||
tryReconnectNow() {
|
||||
this.tryReconnectWithBackoff(USER_ACTIVITY_RECONNECT_DELAY)
|
||||
this.tryReconnectWithBackoff(USER_ACTIVITY_RECONNECT_NOW_DELAY)
|
||||
}
|
||||
|
||||
// Called when document is clicked or the editor cursor changes
|
||||
|
@ -329,9 +330,27 @@ export class ConnectionManager extends Emitter<Events> {
|
|||
inactiveDisconnect: false,
|
||||
lastConnectionAttempt: performance.now(),
|
||||
})
|
||||
|
||||
this.addReconnectListeners()
|
||||
this.socket.socket.connect()
|
||||
}
|
||||
|
||||
private addReconnectListeners() {
|
||||
const handleFailure = () => {
|
||||
removeSocketListeners()
|
||||
this.startAutoReconnectCountdown(0)
|
||||
}
|
||||
const handleSuccess = () => {
|
||||
removeSocketListeners()
|
||||
}
|
||||
const removeSocketListeners = () => {
|
||||
this.socket.removeListener('error', handleFailure)
|
||||
this.socket.removeListener('connect', handleSuccess)
|
||||
}
|
||||
this.socket.on('error', handleFailure)
|
||||
this.socket.on('connect', handleSuccess)
|
||||
}
|
||||
|
||||
private tryReconnectGracefully() {
|
||||
if (
|
||||
this.state.readyState === WebSocket.CLOSED ||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { createContext, FC, useCallback, useContext, useState } from 'react'
|
||||
|
||||
const GlobalAlertsContext = createContext<HTMLDivElement | null | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
export const GlobalAlertsProvider: FC = ({ children }) => {
|
||||
const [globalAlertsContainer, setGlobalAlertsContainer] =
|
||||
useState<HTMLDivElement | null>(null)
|
||||
|
||||
const handleGlobalAlertsContainer = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
setGlobalAlertsContainer(node)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<GlobalAlertsContext.Provider value={globalAlertsContainer}>
|
||||
<div className="global-alerts" ref={handleGlobalAlertsContainer} />
|
||||
{children}
|
||||
</GlobalAlertsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useGlobalAlertsContainer = () => {
|
||||
const context = useContext(GlobalAlertsContext)
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
'useGlobalAlertsContainer is only available inside GlobalAlertsProvider'
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
|
@ -6,6 +6,9 @@
|
|||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat {
|
||||
|
|
Loading…
Add table
Reference in a new issue