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 {