From eb3e5037f8a27740476e9e9c976e364177e28b38 Mon Sep 17 00:00:00 2001 From: Alf Eaton Date: Wed, 3 Jan 2024 11:12:50 +0000 Subject: [PATCH] Merge pull request #16335 from overleaf/ae-real-time-down [ide-react] Improve handling of lost connection GitOrigin-RevId: 89b641b2beca4f9de65551e6873b3c8c11bb1695 --- .../ide-react/components/alerts/alerts.tsx | 14 ++++++-- .../ide-react/components/file-tree.tsx | 4 +-- .../ide-react/components/layout/ide-page.tsx | 5 +-- .../unsaved-docs/unsaved-docs-alert.tsx | 4 +-- .../unsaved-docs-locked-modal.tsx | 1 + .../components/unsaved-docs/unsaved-docs.tsx | 10 +++++- .../connection/connection-manager.ts | 23 ++++++++++-- .../context/global-alerts-context.tsx | 36 +++++++++++++++++++ .../stylesheets/app/editor/ide-react.less | 3 ++ 9 files changed, 88 insertions(+), 12 deletions(-) create mode 100644 services/web/frontend/js/features/ide-react/context/global-alerts-context.tsx diff --git a/services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx b/services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx index d011e1efd3..29e97a2137 100644 --- a/services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx +++ b/services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx @@ -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 ( -
+ if (!globalAlertsContainer) { + return null + } + + return createPortal( + <> {connectionState.forceDisconnected ? ( {t('disconnected')} @@ -67,6 +74,7 @@ export function Alerts() { Connected: {isConnected.toString()} ) : null} -
+ , + globalAlertsContainer ) } diff --git a/services/web/frontend/js/features/ide-react/components/file-tree.tsx b/services/web/frontend/js/features/ide-react/components/file-tree.tsx index 4213074a49..44cd86d3d2 100644 --- a/services/web/frontend/js/features/ide-react/components/file-tree.tsx +++ b/services/web/frontend/js/features/ide-react/components/file-tree.tsx @@ -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} diff --git a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx index 91bd7f92e1..32afd847e2 100644 --- a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx +++ b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx @@ -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 ( - <> + - + ) } diff --git a/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-alert.tsx b/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-alert.tsx index e953d0a910..819c85d5e2 100644 --- a/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-alert.tsx +++ b/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-alert.tsx @@ -6,14 +6,14 @@ import { useTranslation } from 'react-i18next' export const UnsavedDocsAlert: FC<{ unsavedDocs: Map }> = ({ unsavedDocs, }) => ( -
+ <> {[...unsavedDocs.entries()].map( ([docId, seconds]) => seconds > 8 && ( ) )} -
+ ) const UnsavedDocAlert: FC<{ docId: string; seconds: number }> = ({ diff --git a/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-locked-modal.tsx b/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-locked-modal.tsx index c136ec8d30..db3206de1c 100644 --- a/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-locked-modal.tsx +++ b/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-locked-modal.tsx @@ -8,6 +8,7 @@ export const UnsavedDocsLockedModal: FC = () => { return ( {}} // It's not possible to hide this modal, but it's a required prop className="lock-editor-modal" backdrop={false} diff --git a/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs.tsx b/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs.tsx index f0ce93cf11..d71324d6ef 100644 --- a/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs.tsx +++ b/services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs.tsx @@ -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()) + const globalAlertsContainer = useGlobalAlertsContainer() // always contains the latest value const previousUnsavedDocsRef = useRef(unsavedDocs) @@ -99,7 +102,12 @@ export const UnsavedDocs: FC = () => { return ( <> {isLocked && } - {unsavedDocs.size > 0 && } + {unsavedDocs.size > 0 && + globalAlertsContainer && + createPortal( + , + globalAlertsContainer + )} ) } diff --git a/services/web/frontend/js/features/ide-react/connection/connection-manager.ts b/services/web/frontend/js/features/ide-react/connection/connection-manager.ts index 080231744d..c3a6cb1730 100644 --- a/services/web/frontend/js/features/ide-react/connection/connection-manager.ts +++ b/services/web/frontend/js/features/ide-react/connection/connection-manager.ts @@ -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 { } 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 { 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 || diff --git a/services/web/frontend/js/features/ide-react/context/global-alerts-context.tsx b/services/web/frontend/js/features/ide-react/context/global-alerts-context.tsx new file mode 100644 index 0000000000..d2dfefd8c0 --- /dev/null +++ b/services/web/frontend/js/features/ide-react/context/global-alerts-context.tsx @@ -0,0 +1,36 @@ +import { createContext, FC, useCallback, useContext, useState } from 'react' + +const GlobalAlertsContext = createContext( + undefined +) + +export const GlobalAlertsProvider: FC = ({ children }) => { + const [globalAlertsContainer, setGlobalAlertsContainer] = + useState(null) + + const handleGlobalAlertsContainer = useCallback( + (node: HTMLDivElement | null) => { + setGlobalAlertsContainer(node) + }, + [] + ) + + return ( + +
+ {children} + + ) +} + +export const useGlobalAlertsContainer = () => { + const context = useContext(GlobalAlertsContext) + + if (context === undefined) { + throw new Error( + 'useGlobalAlertsContainer is only available inside GlobalAlertsProvider' + ) + } + + return context +} diff --git a/services/web/frontend/stylesheets/app/editor/ide-react.less b/services/web/frontend/stylesheets/app/editor/ide-react.less index cd0c8d64f4..aadc3063c3 100644 --- a/services/web/frontend/stylesheets/app/editor/ide-react.less +++ b/services/web/frontend/stylesheets/app/editor/ide-react.less @@ -6,6 +6,9 @@ top: 0; left: 0; right: 0; + display: flex; + flex-direction: column; + align-items: center; } .chat {