Просмотр исходного кода

Merge pull request #16335 from overleaf/ae-real-time-down

[ide-react] Improve handling of lost connection

GitOrigin-RevId: 89b641b2beca4f9de65551e6873b3c8c11bb1695
Alf Eaton 2 лет назад
Родитель
Сommit
eb3e5037f8

+ 11 - 3
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 (
-    <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
   )
 }

+ 2 - 2
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}

+ 3 - 2
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 (
-    <>
+    <GlobalAlertsProvider>
       <Alerts />
       <Modals />
       <EditorLeftMenu />
       <MainLayout />
-    </>
+    </GlobalAlertsProvider>
   )
 }

+ 2 - 2
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<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 }> = ({

+ 1 - 0
services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs-locked-modal.tsx

@@ -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}

+ 9 - 1
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<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
+        )}
     </>
   )
 }

+ 21 - 2
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<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 ||

+ 36 - 0
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<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
+}

+ 3 - 0
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 {