diff --git a/src/main/classes/controllers/AccountController.ts b/src/main/classes/controllers/AccountController.ts index a563097c..82b730ac 100644 --- a/src/main/classes/controllers/AccountController.ts +++ b/src/main/classes/controllers/AccountController.ts @@ -139,6 +139,10 @@ export class AccountController { return false } + // The saved SIP settings may be stale (e.g. NethVoice updated while NethLink was closed): + // refresh them from the server before the PhoneIsland is built with them + const sipChanged = await this.refreshServerConfig(lastLoggedAccount) + // Update store with the saved account (don't do a new login!) // IMPORTANT: Preserve auth.lastUser and auth.lastUserCryptPsw so they are saved to disk // IMPORTANT: Set connection: true to prevent "No internet connection" banner @@ -154,6 +158,9 @@ export class AccountController { lastUserCryptPsw: authAppData.lastUserCryptPsw } }, 'autoLogin') + if (sipChanged) { + store.saveToDisk() + } return true } else { @@ -176,15 +183,13 @@ export class AccountController { return false } - let loggedAccount: Account = { + const loggedAccount: Account = { ...lastLoggedAccount, ...tempLoggedAccount, theme: lastLoggedAccount.theme || tempLoggedAccount.theme, } - const { parseConfig } = useLogin() - const config: string = await NetworkController.instance.get(`https://${loggedAccount.host}/config/config.production.js`) - loggedAccount = parseConfig(loggedAccount, config) + await this.refreshServerConfig(loggedAccount) await this.saveLoggedAccount(loggedAccount, password) return true } catch (e) { @@ -196,6 +201,48 @@ export class AccountController { return false } + /** + * Re-read the host config.production.js and update the SIP settings of the given account in place. + * It never throws: on network or parsing errors the cached values are kept. + * @returns true when sipHost or sipPort changed + */ + async refreshServerConfig(account: Account): Promise { + try { + const { parseConfig } = useLogin() + const config: string = await NetworkController.instance.get(`https://${account.host}/config/config.production.js`) + const previous = { sipHost: account.sipHost, sipPort: account.sipPort } + parseConfig(account, config) + const changed = previous.sipHost !== account.sipHost || previous.sipPort !== account.sipPort + if (changed) { + Log.info('server SIP config changed', { previous, current: { sipHost: account.sipHost, sipPort: account.sipPort } }) + } + return changed + } catch (e) { + Log.warning('unable to refresh server config, keeping cached SIP settings:', e) + return false + } + } + + /** + * Refresh the SIP settings of the logged account from the server and persist them if changed. + * @returns true when sipHost or sipPort changed + */ + async syncLoggedAccountServerConfig(): Promise { + const account = store.store.account + if (!account) return false + const changed = await this.refreshServerConfig(account) + if (changed) { + store.set('account', account, true) + const auth = store.store.auth + if (auth) { + auth.availableAccounts[getAccountUID(account)] = account + store.set('auth', auth, true) + } + store.saveToDisk() + } + return changed + } + async saveLoggedAccount(account: Account, password: string): Promise { try { // Wait for encryption to become available (max 10 seconds with 500ms intervals) diff --git a/src/main/classes/controllers/PhoneIslandController.ts b/src/main/classes/controllers/PhoneIslandController.ts index 4a3faf39..659268c9 100644 --- a/src/main/classes/controllers/PhoneIslandController.ts +++ b/src/main/classes/controllers/PhoneIslandController.ts @@ -365,20 +365,6 @@ export class PhoneIslandController { } } - reconnect() { - try { - Log.info('PHONE ISLAND RECONNECT') - this.window.emit(IPC_EVENTS.RECONNECT_PHONE_ISLAND) - once(IPC_EVENTS.LOGOUT_COMPLETED, () => { - Log.info('PHONE ISLAND RECONNECTION AFTER LOGOUT') - this.window.quit(false) - new PhoneIslandController() - }) - } catch (e) { - Log.warning('error during emitting reconnect event to the PhoneIslandWindow:', e) - } - } - muteAudio() { try { const window = this.window.getWindow() diff --git a/src/main/lib/ipcEvents.ts b/src/main/lib/ipcEvents.ts index fc608d14..3db2950b 100644 --- a/src/main/lib/ipcEvents.ts +++ b/src/main/lib/ipcEvents.ts @@ -35,6 +35,28 @@ export function isCallActive(): boolean { return hasActiveCall } +// Set when the SIP settings changed during a call: the PhoneIsland reload is deferred to the call end +let isPhoneIslandReloadPending = false + +// Minimum interval between two server config checks triggered by PhoneIsland connection errors +const SERVER_CONFIG_CHECK_MIN_INTERVAL = 10_000 +let lastServerConfigCheck = 0 + +/** + * Reload the PhoneIsland so that it registers again with the current SIP host/port. + * When a call is active the reload is deferred to the call end, since it would drop the call. + */ +function reloadPhoneIslandForSipChange(trigger: string) { + if (isCallActive()) { + Log.info(`SIP config changed (${trigger}) but a call is active - PhoneIsland reload deferred to call end`) + isPhoneIslandReloadPending = true + return + } + isPhoneIslandReloadPending = false + Log.info(`SIP config changed (${trigger}) - reloading PhoneIsland`) + PhoneIslandController.instance?.window?.getWindow()?.reload() +} + function onSyncEmitter( channel: IPC_EVENTS, asyncCallback: (...args: any[]) => Promise @@ -475,6 +497,14 @@ export function registerIpcEvents() { ipcMain.on(IPC_EVENTS.EMIT_CALL_END, (_) => { Log.info('Call ended - setting hasActiveCall = false') hasActiveCall = false + if (isPhoneIslandReloadPending) { + // give the PhoneIsland the time to close the call UI before reloading it + setTimeout(() => { + if (isPhoneIslandReloadPending) { + reloadPhoneIslandForSipChange('call ended') + } + }, 1000) + } try { NethLinkController.instance.window.emit(IPC_EVENTS.EMIT_CALL_END) } catch (e) { @@ -508,13 +538,37 @@ export function registerIpcEvents() { ipcMain.on(IPC_EVENTS.RECONNECT_SOCKET, async () => { try { + const previous = { sipHost: store.store.account?.sipHost, sipPort: store.store.account?.sipPort } + // autoLogin refreshes the SIP settings from the server config too await AccountController.instance.autoLogin() NethLinkController.instance.window.emit(IPC_EVENTS.RECONNECT_SOCKET) + const account = store.store.account + if (account && (account.sipHost !== previous.sipHost || account.sipPort !== previous.sipPort)) { + reloadPhoneIslandForSipChange('socket reconnected') + } } catch (e) { Log.error('SOCKET Reconnection error on logout', e) } }) + ipcMain.on(IPC_EVENTS.CHECK_SERVER_CONFIG, async () => { + // A PhoneIsland WebRTC failure may be caused by SIP settings changed on the server (e.g. NethVoice + // update while NethLink is running): re-read them and reload the PhoneIsland if they changed. + const now = Date.now() + if (now - lastServerConfigCheck < SERVER_CONFIG_CHECK_MIN_INTERVAL) { + return + } + lastServerConfigCheck = now + try { + const changed = await AccountController.instance.syncLoggedAccountServerConfig() + if (changed) { + reloadPhoneIslandForSipChange('webrtc down') + } + } catch (e) { + Log.error('CHECK_SERVER_CONFIG error', e) + } + }) + ipcMain.on(IPC_EVENTS.FULLSCREEN_ENTER, () => { try { PhoneIslandController.instance.window.getWindow()?.setFullScreen(true); diff --git a/src/renderer/src/hooks/usePhoneIslandEventListeners.ts b/src/renderer/src/hooks/usePhoneIslandEventListeners.ts index 3e0d0887..ec86f0f3 100644 --- a/src/renderer/src/hooks/usePhoneIslandEventListeners.ts +++ b/src/renderer/src/hooks/usePhoneIslandEventListeners.ts @@ -336,6 +336,13 @@ export const usePhoneIslandEventListener = () => { // Check if both WebRTC and Socket are ready checkAndTriggerPhoneIslandReady() }), + ...eventHandler(PHONE_ISLAND_EVENTS["phone-island-alert-set"], (data) => { + // A WebRTC failure may be due to SIP settings changed on the server (e.g. after a NethVoice + // update): ask the main process to re-check the server config + if (data?.type === 'webrtc_down') { + window.electron.send(IPC_EVENTS.CHECK_SERVER_CONFIG) + } + }), ...eventHandler(PHONE_ISLAND_EVENTS["phone-island-all-alerts-removed"]), ...eventHandler(PHONE_ISLAND_EVENTS["phone-island-fullscreen-entered"], () => { window.electron.send(IPC_EVENTS.FULLSCREEN_ENTER) diff --git a/src/renderer/src/pages/PhoneIslandPage.tsx b/src/renderer/src/pages/PhoneIslandPage.tsx index 510c9248..da8eb0f1 100644 --- a/src/renderer/src/pages/PhoneIslandPage.tsx +++ b/src/renderer/src/pages/PhoneIslandPage.tsx @@ -203,10 +203,6 @@ export function PhoneIslandPage() { }) }) - window.electron.receive(IPC_EVENTS.RECONNECT_PHONE_ISLAND, () => { - logout() - }) - window.electron.receive(IPC_EVENTS.CHANGE_DEFAULT_DEVICE, async (deviceInformationObject, force) => { Log.debug('CHANGE_DEFAULT_DEVICE', { force, deviceInformationObject, }) const changed = await NethVoiceAPI.User.default_device(deviceInformationObject, force) diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 0d8d372f..f44529ed 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -65,8 +65,8 @@ export enum IPC_EVENTS { REQUEST_SHARED_STATE = "REQUEST_SHARED_STATE", GET_NETHVOICE_CONFIG = "GET_NETHVOICE_CONFIG", SET_NETHVOICE_CONFIG = "SET_NETHVOICE_CONFIG", - RECONNECT_PHONE_ISLAND = "RECONNECT_PHONE_ISLAND", RECONNECT_SOCKET = "RECONNECT_SOCKET", + CHECK_SERVER_CONFIG = "CHECK_SERVER_CONFIG", LOGOUT_COMPLETED = "LOGOUT_COMPLETED", SHOW_NO_CONNECTION = "SHOW_NO_CONNECTION", HIDE_NO_CONNECTION = "HIDE_NO_CONNECTION", @@ -237,6 +237,7 @@ export enum PHONE_ISLAND_EVENTS { 'phone-island-internet-connected' = 'phone-island-internet-connected', 'phone-island-internet-disconnected' = 'phone-island-internet-disconnected', // Alerts + 'phone-island-alert-set' = 'phone-island-alert-set', 'phone-island-all-alerts-removed' = 'phone-island-all-alerts-removed', // Videocall 'phone-island-fullscreen-entered' = 'phone-island-fullscreen-entered',