Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands",
"@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light",
"@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid",
"@nethesis/phone-island": "^0.18.2",
"@nethesis/phone-island": "^0.18.4",
"@tailwindcss/forms": "^0.5.7",
"@types/lodash": "^4.14.202",
"@types/node": "^18.19.9",
Expand Down
1 change: 1 addition & 0 deletions public/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"This field must be at least": "This field must be at least {{number}} characters",
"This is not a phone number": "This is not a phone number",
"Quit": "Quit",
"Retry": "Retry",
"No internet connection title": "No internet connection",
"No internet connection description": "Please check your connection and try again",
"Refresh": "Refresh",
Expand Down
1 change: 1 addition & 0 deletions public/locales/it/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"This field must be at least": "Questo campo deve avere almeno {{number}} caratteri",
"This is not a phone number": "Questo non é un numero telefonico",
"Quit": "Chiudi",
"Retry": "Riprova",
"No internet connection title": "Nessuna connessione internet",
"No internet connection description": "Si prega di controllare la connessione e riprovare",
"Refresh": "Aggiorna",
Expand Down
13 changes: 13 additions & 0 deletions src/main/classes/controllers/NetworkController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,17 @@ export class NetworkController {
}
}

async head(path: string, timeoutMs: number = 5000): Promise<boolean> {
try {
await axios.head(path, {
timeout: timeoutMs
})
return true
} catch (e: any) {
const err: AxiosError = e
Log.debug('during fetch HEAD', err.name, err.code, err.message, path)
return false
}
}

}
88 changes: 79 additions & 9 deletions src/main/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { app, ipcMain, nativeTheme, powerMonitor, protocol, systemPreferences, dialog, shell, globalShortcut } from 'electron'
import { app, ipcMain, nativeTheme, powerMonitor, protocol, systemPreferences, dialog, shell, globalShortcut, net } from 'electron'
import { registerIpcEvents, isCallActive, disableCommandBarShortcuts } from '@/lib/ipcEvents'
import { AccountController } from './classes/controllers'
import { PhoneIslandController } from './classes/controllers/PhoneIslandController'
Expand Down Expand Up @@ -289,6 +289,9 @@ function attachOnReadyProcess() {
}
})

// Track if we're waiting for connection (dialog is shown)
let waitingForConnection = false

async function startApp(attempt = 0) {
let data = store.store || store.getFromDisk()
if (!checkData(data)) {
Expand All @@ -311,13 +314,19 @@ function attachOnReadyProcess() {
const isOnline = await checkConnection()
Log.info('START - START APP, retry:', attempt)
if (!isOnline) {
Log.info('START - NO CONNECTION', attempt, store.store)
Log.info('START - NO CONNECTION', attempt)
if (attempt >= 3) {
// Stop retrying and show the no connection dialog
Log.info('START - showing no connection dialog, stopping automatic retries')
waitingForConnection = true
startConnectionPolling()
try {
SplashScreenController.instance.window.emit(IPC_EVENTS.SHOW_NO_CONNECTION)
} catch (e) {
Log.error(e)
}
// Don't continue retrying - wait for user to click Retry or network to come back
return
}

retryAppStart = setTimeout(() => {
Expand Down Expand Up @@ -366,6 +375,48 @@ function attachOnReadyProcess() {
}
}

// Polling interval for checking connection when waiting
let connectionCheckInterval: NodeJS.Timeout | null = null

// Start polling for connection when dialog is shown
function startConnectionPolling() {
if (connectionCheckInterval) return
connectionCheckInterval = setInterval(async () => {
if (!waitingForConnection) {
stopConnectionPolling()
return
}
// Reuse checkConnection which has fallback logic
const connected = await checkConnection()
if (connected) {
Log.info('START - network came back online, retrying automatically')
waitingForConnection = false
stopConnectionPolling()
try {
SplashScreenController.instance.window.emit(IPC_EVENTS.HIDE_NO_CONNECTION)
} catch (e) {
// Splash screen might be closed
}
startApp(0)
}
}, 5000) // Check every 5 seconds
}

function stopConnectionPolling() {
if (connectionCheckInterval) {
clearInterval(connectionCheckInterval)
connectionCheckInterval = null
}
}

// Listen for retry connection event from the splash screen
ipcMain.on(IPC_EVENTS.RETRY_CONNECTION, () => {
Log.info('START - user requested connection retry')
waitingForConnection = false
stopConnectionPolling()
startApp(0)
})

app.on('window-all-closed', () => {
app.dock?.hide()
})
Expand Down Expand Up @@ -807,14 +858,33 @@ function checkData(data: any): boolean {

}

const CONNECTIVITY_CHECK_ENDPOINTS = [
'https://connectivitycheck.gstatic.com/generate_204', // Google's connectivity check
'https://1.1.1.1/cdn-cgi/trace', // Cloudflare
'https://cloudflare.com/cdn-cgi/trace' // Cloudflare alternative
]

async function checkConnection() {
const connected = await new Promise((resolve) => {
NetworkController.instance.get('https://google.com', {} as any).then(() => {
resolve(true)
}).catch(() => {
resolve(false)
})
})
// Quick check using Electron's built-in net.isOnline()
if (!net.isOnline()) {
Log.debug("checkConnection: net.isOnline() returned false")
if (store.store.connection !== false) {
ipcMain.emit(IPC_EVENTS.UPDATE_CONNECTION_STATE, undefined, false);
}
return false
}

// Try connectivity check endpoints with fallbacks
let connected = false
for (const endpoint of CONNECTIVITY_CHECK_ENDPOINTS) {
connected = await NetworkController.instance.head(endpoint, 3000)
if (connected) {
Log.debug("checkConnection: succeeded with", endpoint)
break
}
Log.debug("checkConnection: failed with", endpoint, "trying next...")
}

Log.debug("checkConnection:", { connected, connection: store.store.connection })
if (connected !== store.store.connection) {
ipcMain.emit(IPC_EVENTS.UPDATE_CONNECTION_STATE, undefined, connected);
Expand Down
1 change: 1 addition & 0 deletions src/renderer/public/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"This field must be at least": "This field must be at least {{number}} characters",
"This is not a phone number": "This is not a phone number",
"Quit": "Quit",
"Retry": "Retry",
"No internet connection title": "No internet connection",
"No internet connection description": "Please check your connection and try again",
"Refresh": "Refresh",
Expand Down
1 change: 1 addition & 0 deletions src/renderer/public/locales/it/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"This field must be at least": "Questo campo deve avere almeno {{number}} caratteri",
"This is not a phone number": "Questo non é un numero telefonico",
"Quit": "Chiudi",
"Retry": "Riprova",
"No internet connection title": "Nessuna connessione internet",
"No internet connection description": "Si prega di controllare la connessione e riprovare",
"Refresh": "Aggiorna",
Expand Down
35 changes: 27 additions & 8 deletions src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,35 @@ const RequestStateComponent = () => {
const [account,] = useSharedState('account')
const [connection,] = useSharedState('connection')
const [hasWindowConfig, setHasWindowConfig] = useState<boolean>(false)
const { GET } = useNetwork()
const { HEAD } = useNetwork()

const CONNECTIVITY_CHECK_ENDPOINTS = [
'https://connectivitycheck.gstatic.com/generate_204', // Google's connectivity check
'https://1.1.1.1/cdn-cgi/trace', // Cloudflare
'https://cloudflare.com/cdn-cgi/trace' // Cloudflare alternative
]

async function checkConnection() {
const connected = await new Promise((resolve) => {
GET('https://google.com', {} as any).then(() => {
resolve(true)
}).catch(() => {
resolve(false)
})
})
// Quick check using browser's navigator.onLine
if (!navigator.onLine) {
Log.debug('check connection: navigator.onLine returned false')
if (connection !== false) {
window.electron.send(IPC_EVENTS.UPDATE_CONNECTION_STATE, false);
}
return
}

// Try connectivity check endpoints with fallbacks
let connected = false
for (const endpoint of CONNECTIVITY_CHECK_ENDPOINTS) {
connected = await HEAD(endpoint, 3000)
if (connected) {
Log.debug('check connection: succeeded with', endpoint)
break
}
Log.debug('check connection: failed with', endpoint, 'trying next...')
}

Log.debug('check connection', { connected, connection: connection })
if (connected !== connection) {
window.electron.send(IPC_EVENTS.UPDATE_CONNECTION_STATE, connected);
Expand Down
16 changes: 9 additions & 7 deletions src/renderer/src/pages/SplashScreenPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import darkLogo from '../assets/nethvoiceDarkIcon.svg'
import lightLogo from '../assets/nethvoiceLightIcon.svg'
import { t } from 'i18next'
import { useState } from 'react'
import { useSharedState } from '@renderer/store'
import { IPC_EVENTS } from '@shared/constants'
import { ConnectionErrorDialog } from '@renderer/components'

Expand All @@ -16,16 +15,19 @@ export interface SplashScreenPageProps {
}

export function SplashScreenPage({ themeMode }: SplashScreenPageProps) {
const [connection] = useSharedState('connection')
const [isNoConnectionDialogOpen, setIsnoConnectionDialogOpen] = useState<boolean>(false)
useInitialize(() => {
window.electron.receive(IPC_EVENTS.SHOW_NO_CONNECTION, () => {
setIsnoConnectionDialogOpen(true)
})
window.electron.receive(IPC_EVENTS.HIDE_NO_CONNECTION, () => {
setIsnoConnectionDialogOpen(false)
})
})

function exitApp() {
window.api.exitNethLink()
function retryConnection() {
setIsnoConnectionDialogOpen(false)
window.electron.send(IPC_EVENTS.RETRY_CONNECTION)
}

return (
Expand All @@ -36,11 +38,11 @@ export function SplashScreenPage({ themeMode }: SplashScreenPageProps) {
className="absolute w-screen h-screen top-0 left-0 object-cover"
/>
<div className="absolute top-0 left-0 w-screen h-screen">
{isNoConnectionDialogOpen && !connection && (
{isNoConnectionDialogOpen && (
<ConnectionErrorDialog
variant='splashscreen'
onButtonClick={exitApp}
buttonText={t('Common.Quit')}
onButtonClick={retryConnection}
buttonText={t('Common.Refresh')}
/>
)
}
Expand Down
2 changes: 2 additions & 0 deletions src/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export enum IPC_EVENTS {
RECONNECT_SOCKET = "RECONNECT_SOCKET",
LOGOUT_COMPLETED = "LOGOUT_COMPLETED",
SHOW_NO_CONNECTION = "SHOW_NO_CONNECTION",
HIDE_NO_CONNECTION = "HIDE_NO_CONNECTION",
RETRY_CONNECTION = "RETRY_CONNECTION",
UPDATE_CONNECTION_STATE = "UPDATE_CONNECTION_STATE",
DEV_TOOL_TOGGLE_CONNECTION = "DEV_TOOL_TOGGLE_CONNECTION",
START_DRAG = "START_DRAG",
Expand Down
16 changes: 15 additions & 1 deletion src/shared/useNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,22 @@ export const useNetwork = () => {
}
}

async function HEAD(path: string, timeoutMs: number = 5000): Promise<boolean> {
try {
await axios.head(path, {
timeout: timeoutMs
})
return true
} catch (e: any) {
const err: AxiosError = e
Log.debug('during fetch HEAD', err.name, err.code, err.message, path)
return false
}
}

return {
GET,
POST
POST,
HEAD
}
}